envio 3.12.1 → 3.13.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -8
- package/src/BatchProcessing.res +15 -14
- package/src/BatchProcessing.res.mjs +8 -15
- package/src/Bin.res +24 -17
- package/src/Bin.res.mjs +5 -0
- package/src/ChainFetching.res +18 -24
- package/src/ChainFetching.res.mjs +14 -15
- package/src/ChainState.res +103 -38
- package/src/ChainState.res.mjs +79 -22
- package/src/ChainState.resi +14 -2
- package/src/Config.res +32 -0
- package/src/Config.res.mjs +24 -0
- package/src/Core.res +4 -0
- package/src/Core.res.mjs +4 -0
- package/src/CrossChainState.res +109 -11
- package/src/CrossChainState.res.mjs +71 -7
- package/src/CrossChainState.resi +11 -1
- package/src/Env.res +4 -0
- package/src/ErrorHandling.res +15 -4
- package/src/ErrorHandling.res.mjs +16 -5
- package/src/FinalizeBackfill.res +10 -4
- package/src/FinalizeBackfill.res.mjs +5 -3
- package/src/HandlerRegister.res +0 -37
- package/src/HandlerRegister.res.mjs +0 -18
- package/src/IndexerLoop.res +2 -0
- package/src/IndexerLoop.res.mjs +1 -0
- package/src/IndexerState.res +43 -1
- package/src/IndexerState.res.mjs +48 -4
- package/src/IndexerState.resi +11 -0
- package/src/Main.res +128 -268
- package/src/Main.res.mjs +27 -153
- package/src/Metrics.res +268 -102
- package/src/Metrics.res.mjs +227 -48
- package/src/Persistence.res +30 -2
- package/src/Persistence.res.mjs +12 -3
- package/src/PgStorage.res +19 -11
- package/src/PgStorage.res.mjs +13 -10
- package/src/Rollback.res +3 -3
- package/src/Server.res +178 -0
- package/src/Server.res.mjs +143 -0
- package/src/SimulateItems.res +1 -1
- package/src/SimulateItems.res.mjs +1 -1
- package/src/Supervisor.res +542 -0
- package/src/Supervisor.res.mjs +455 -0
- package/src/TestIndexer.res +1 -1
- package/src/TestIndexer.res.mjs +2 -2
- package/src/Worker.res +101 -0
- package/src/Worker.res.mjs +81 -0
- package/src/bindings/NodeJs.res +50 -0
- package/src/bindings/NodeJs.res.mjs +8 -1
- package/src/db/InternalTable.res +8 -1
- package/src/db/InternalTable.res.mjs +5 -1
- package/src/sources/AddressSet.res +5 -3
- package/src/sources/AddressStore.res +19 -11
- package/src/sources/AddressStore.res.mjs +2 -7
- package/src/sources/BlockStore.res +10 -9
- package/src/sources/BlockStore.res.mjs +4 -4
- package/src/sources/ChainSources.res +4 -0
- package/src/sources/ChainSources.res.mjs +2 -2
- package/src/sources/EvmChain.res +4 -0
- package/src/sources/EvmChain.res.mjs +3 -1
- package/src/sources/EvmEventItem.res +56 -0
- package/src/sources/EvmEventItem.res.mjs +32 -0
- package/src/sources/EvmHyperSyncSource.res +7 -37
- package/src/sources/EvmHyperSyncSource.res.mjs +8 -29
- package/src/sources/EvmRpcClient.res +41 -59
- package/src/sources/EvmRpcClient.res.mjs +4 -54
- package/src/sources/EvmRpcWs.res +4 -1
- package/src/sources/EvmRpcWs.res.mjs +6 -2
- package/src/sources/FuelHyperSyncSource.res +6 -4
- package/src/sources/FuelHyperSyncSource.res.mjs +7 -9
- package/src/sources/HeightFeed.res +7 -1
- package/src/sources/HeightFeed.res.mjs +9 -7
- package/src/sources/HyperSyncClient.res +1 -14
- package/src/sources/RequestStat.res +7 -0
- package/src/sources/RequestStat.res.mjs +14 -1
- package/src/sources/RpcSource.res +100 -1043
- package/src/sources/RpcSource.res.mjs +81 -1056
- package/src/sources/SimulateSource.res +2 -5
- package/src/sources/SimulateSource.res.mjs +2 -3
- package/src/sources/Source.res +34 -12
- package/src/sources/Source.res.mjs +11 -0
- package/src/sources/SourceManager.res +111 -31
- package/src/sources/SourceManager.res.mjs +65 -31
- package/src/sources/SourceManager.resi +4 -0
- package/src/sources/StartBlockResolver.res +8 -5
- package/src/sources/StartBlockResolver.res.mjs +4 -2
- package/src/sources/SvmHyperSyncSource.res +4 -2
- package/src/sources/SvmHyperSyncSource.res.mjs +4 -6
- package/src/sources/TransactionStore.res +9 -6
- package/src/sources/TransactionStore.res.mjs +2 -2
- package/src/tui/Tui.res +10 -0
- package/src/tui/Tui.res.mjs +15 -0
- package/src/tui/components/SyncETA.res +12 -6
- package/src/tui/components/SyncETA.res.mjs +12 -8
- package/src/LazyLoader.res +0 -135
- package/src/LazyLoader.res.mjs +0 -118
- package/src/bindings/SDSL.res +0 -12
- package/src/bindings/SDSL.res.mjs +0 -9
- package/src/bindings/Yargs.res +0 -8
- package/src/bindings/Yargs.res.mjs +0 -2
- package/src/sources/Rpc.res +0 -185
- package/src/sources/Rpc.res.mjs +0 -183
package/src/Config.res.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import * as Logging from "./Logging.res.mjs";
|
|
|
13
13
|
import * as ChainMap from "./ChainMap.res.mjs";
|
|
14
14
|
import * as Internal from "./Internal.res.mjs";
|
|
15
15
|
import * as BigDecimal from "./bindings/BigDecimal.res.mjs";
|
|
16
|
+
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
16
17
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
17
18
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
18
19
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
@@ -470,6 +471,11 @@ function getChain(config, chainId) {
|
|
|
470
471
|
}
|
|
471
472
|
}
|
|
472
473
|
|
|
474
|
+
function isPerChain(config) {
|
|
475
|
+
let match = config.checkpointSequence;
|
|
476
|
+
return match !== "SharedAcrossChains";
|
|
477
|
+
}
|
|
478
|
+
|
|
473
479
|
function isolate(config, chainIds) {
|
|
474
480
|
let shared = config.userEntities.filter(entityConfig => entityConfig.crossChain);
|
|
475
481
|
if (shared.length !== 0) {
|
|
@@ -907,6 +913,17 @@ function prime(json) {
|
|
|
907
913
|
cached.contents = undefined;
|
|
908
914
|
}
|
|
909
915
|
|
|
916
|
+
function withCommandFields(json, chainIds, isDev) {
|
|
917
|
+
let fields = Stdlib_JSON.Decode.object(json);
|
|
918
|
+
if (fields === undefined) {
|
|
919
|
+
return Stdlib_JsError.throwWithMessage("Invalid indexer config: not an object");
|
|
920
|
+
}
|
|
921
|
+
let narrowed = Object.assign({}, fields);
|
|
922
|
+
narrowed["isolatedChains"] = S$RescriptSchema.reverseConvertToJsonOrThrow(chainIds, S$RescriptSchema.array(ChainId.schema));
|
|
923
|
+
narrowed["isDev"] = isDev;
|
|
924
|
+
return narrowed;
|
|
925
|
+
}
|
|
926
|
+
|
|
910
927
|
function getPublicConfigJson() {
|
|
911
928
|
let json = primedJson.contents;
|
|
912
929
|
if (json !== undefined) {
|
|
@@ -950,6 +967,10 @@ function stripSensitiveData(json) {
|
|
|
950
967
|
return cloned;
|
|
951
968
|
}
|
|
952
969
|
|
|
970
|
+
function envioInfo() {
|
|
971
|
+
return stripSensitiveData(getPublicConfigJson());
|
|
972
|
+
}
|
|
973
|
+
|
|
953
974
|
function canonicalJson(json) {
|
|
954
975
|
if (Array.isArray(json)) {
|
|
955
976
|
return json.map(canonicalJson);
|
|
@@ -1172,14 +1193,17 @@ export {
|
|
|
1172
1193
|
publicConfigSchema,
|
|
1173
1194
|
contractMappingOf,
|
|
1174
1195
|
getChain,
|
|
1196
|
+
isPerChain,
|
|
1175
1197
|
isolate,
|
|
1176
1198
|
fromPublic,
|
|
1177
1199
|
normalizeUserAddress,
|
|
1178
1200
|
normalizeSimulateAddress,
|
|
1179
1201
|
getEventConfig,
|
|
1180
1202
|
prime,
|
|
1203
|
+
withCommandFields,
|
|
1181
1204
|
getPublicConfigJson,
|
|
1182
1205
|
stripSensitiveData,
|
|
1206
|
+
envioInfo,
|
|
1183
1207
|
canonicalJson,
|
|
1184
1208
|
diffPaths,
|
|
1185
1209
|
throwIfIncompatible,
|
package/src/Core.res
CHANGED
|
@@ -166,6 +166,10 @@ let loadDevAddon: ({..}, string) => addon = %raw(`function(req, envioDir) {
|
|
|
166
166
|
fs.copyFileSync(srcPath, nodePath);
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
// Forked workers inherit this, so only the first process in a run pays for
|
|
170
|
+
// the cargo build (and they don't contend over the cargo lock).
|
|
171
|
+
process.env.ENVIO_DEV_ADDON = nodePath;
|
|
172
|
+
|
|
169
173
|
return req(nodePath);
|
|
170
174
|
}`)
|
|
171
175
|
|
package/src/Core.res.mjs
CHANGED
|
@@ -88,6 +88,10 @@ let loadDevAddon = (function(req, envioDir) {
|
|
|
88
88
|
fs.copyFileSync(srcPath, nodePath);
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
// Forked workers inherit this, so only the first process in a run pays for
|
|
92
|
+
// the cargo build (and they don't contend over the cargo lock).
|
|
93
|
+
process.env.ENVIO_DEV_ADDON = nodePath;
|
|
94
|
+
|
|
91
95
|
return req(nodePath);
|
|
92
96
|
});
|
|
93
97
|
|
package/src/CrossChainState.res
CHANGED
|
@@ -17,6 +17,11 @@ type t = {
|
|
|
17
17
|
mutable isCaughtUp: bool,
|
|
18
18
|
// Indexer-wide fetch buffer pool (item count), shared across all chains.
|
|
19
19
|
targetBufferSize: int,
|
|
20
|
+
// Set on a process driving part of a split run: the chains it drives may be
|
|
21
|
+
// at the head while chains in another process are still backfilling, and an
|
|
22
|
+
// indexer switches to realtime as a whole or not at all. Cleared by the
|
|
23
|
+
// supervisor once every chain in the run has arrived.
|
|
24
|
+
mutable holdRealtime: bool,
|
|
20
25
|
}
|
|
21
26
|
|
|
22
27
|
// The whole-indexer fetch buffer pool, independent of chain count.
|
|
@@ -26,16 +31,50 @@ let calculateTargetBufferSize = () =>
|
|
|
26
31
|
| None => 100_000
|
|
27
32
|
}
|
|
28
33
|
|
|
29
|
-
let make = (
|
|
34
|
+
let make = (
|
|
35
|
+
~chainStates,
|
|
36
|
+
~isRealtime,
|
|
37
|
+
~targetBufferSize=calculateTargetBufferSize(),
|
|
38
|
+
~holdRealtime=false,
|
|
39
|
+
): t => {
|
|
30
40
|
{
|
|
31
41
|
chainStates,
|
|
32
42
|
chainIds: chainStates->Dict.valuesToArray->Array.map(cs => (cs->ChainState.chainConfig).id),
|
|
33
43
|
isRealtime,
|
|
34
44
|
isCaughtUp: isRealtime,
|
|
35
45
|
targetBufferSize,
|
|
46
|
+
holdRealtime,
|
|
36
47
|
}
|
|
37
48
|
}
|
|
38
49
|
|
|
50
|
+
// The supervisor's go-ahead: every chain in the run has reached the head, so
|
|
51
|
+
// this process may make the transitions it has been holding back.
|
|
52
|
+
let releaseRealtime = (crossChainState: t) => crossChainState.holdRealtime = false
|
|
53
|
+
|
|
54
|
+
let isHoldingRealtime = (crossChainState: t) => crossChainState.holdRealtime
|
|
55
|
+
|
|
56
|
+
// Whether this process has got as far as it can without the run's leave. What a
|
|
57
|
+
// supervisor reads to decide that a split run may go realtime as one.
|
|
58
|
+
//
|
|
59
|
+
// Three ways to have arrived, because a chain can be as far along as it can get
|
|
60
|
+
// in three different states. Its chains have caught up; or it resumed already
|
|
61
|
+
// realtime; or every chain is waiting to enter the reorg threshold, which is as
|
|
62
|
+
// far as one can fetch while the pre-threshold lag holds it at the safe block —
|
|
63
|
+
// entering the threshold is what lifts that lag, so a run held until its chains
|
|
64
|
+
// reached the head would be holding back the transition that gets them there.
|
|
65
|
+
//
|
|
66
|
+
// The process's own conclusion rather than a reading a supervisor reassembles:
|
|
67
|
+
// a chain committed at what was the head and resumed once the head had moved on
|
|
68
|
+
// has arrived, and no live reading of it can say so — which is the same reason
|
|
69
|
+
// `markCaughtUpOnResume` decides before any source request.
|
|
70
|
+
let hasArrivedAtHead = (crossChainState: t) =>
|
|
71
|
+
crossChainState.isCaughtUp ||
|
|
72
|
+
crossChainState.isRealtime || {
|
|
73
|
+
let chainStates = crossChainState.chainStates->Dict.valuesToArray
|
|
74
|
+
chainStates->Utils.Array.notEmpty &&
|
|
75
|
+
chainStates->Array.every(ChainState.isReadyToEnterReorgThreshold)
|
|
76
|
+
}
|
|
77
|
+
|
|
39
78
|
// Resolve a chain's state by id. The id always comes from `chainIds`, which is
|
|
40
79
|
// derived from `chainStates`, so the entry is guaranteed present.
|
|
41
80
|
let getChainState = (crossChainState: t, chainId) =>
|
|
@@ -122,13 +161,29 @@ let createBatch = (
|
|
|
122
161
|
|
|
123
162
|
// Enter the reorg threshold: shrink each chain's buffer by its configured
|
|
124
163
|
// blockLag and flip the flag.
|
|
125
|
-
|
|
126
|
-
|
|
164
|
+
// Whether every chain this process drives has buffered close enough to the head
|
|
165
|
+
// to enter the threshold together — and, in a split run, whether the rest of the
|
|
166
|
+
// run has too. Chains enter it as one indexer, so one chain still backfilling
|
|
167
|
+
// holds the others back whatever process it runs in.
|
|
168
|
+
let isReadyToEnterReorgThreshold = (crossChainState: t, ~batch) =>
|
|
169
|
+
!crossChainState.holdRealtime &&
|
|
170
|
+
crossChainState.chainStates
|
|
171
|
+
->Dict.valuesToArray
|
|
172
|
+
->Array.every(cs => cs->ChainState.isReadyToEnterReorgThresholdAfterBatch(~batch))
|
|
127
173
|
|
|
174
|
+
// Said by each chain rather than once for the indexer: what crossing changes
|
|
175
|
+
// is a chain's own, and the chains of a split run cross in processes that can
|
|
176
|
+
// only speak for the ones they drive.
|
|
177
|
+
let enterReorgThreshold = (crossChainState: t) => {
|
|
128
178
|
for i in 0 to crossChainState.chainIds->Array.length - 1 {
|
|
129
|
-
crossChainState
|
|
130
|
-
|
|
131
|
-
|
|
179
|
+
let cs = crossChainState->getChainState(crossChainState.chainIds->Array.getUnsafe(i))
|
|
180
|
+
// A chain whose end block sits below these blocks says nothing: crossing
|
|
181
|
+
// gives it nothing more to index, and it will never reach one of them.
|
|
182
|
+
let liftsCeiling = cs->ChainState.reorgThresholdLiftsCeiling
|
|
183
|
+
cs->ChainState.enterReorgThreshold
|
|
184
|
+
if liftsCeiling {
|
|
185
|
+
cs->ChainState.logger->Logging.childInfo(ChainState.reorgThresholdEntryMessage)
|
|
186
|
+
}
|
|
132
187
|
}
|
|
133
188
|
}
|
|
134
189
|
|
|
@@ -150,7 +205,10 @@ let applyBatchProgress = (crossChainState: t, ~batch: Batch.t, ~blockTimestampNa
|
|
|
150
205
|
}
|
|
151
206
|
|
|
152
207
|
crossChainState.isCaughtUp =
|
|
153
|
-
crossChainState.isCaughtUp ||
|
|
208
|
+
crossChainState.isCaughtUp ||
|
|
209
|
+
(!crossChainState.holdRealtime &&
|
|
210
|
+
crossChainState->nextItemIsNone &&
|
|
211
|
+
everyChainCaughtUp.contents)
|
|
154
212
|
}
|
|
155
213
|
|
|
156
214
|
// Every chain has buffered up to its head (or endblock) with nothing
|
|
@@ -174,8 +232,13 @@ let isSettledAtHead = (crossChainState: t) => {
|
|
|
174
232
|
}
|
|
175
233
|
|
|
176
234
|
// Enter the FinalizingIndexes phase without a batch, for the resume above.
|
|
235
|
+
// Not while the run holds this process back: the hold keeps the pre-threshold
|
|
236
|
+
// lag in place, and a chain that has fetched to a lagged head it was never
|
|
237
|
+
// going to get past reads as settled without having indexed anything. What a
|
|
238
|
+
// held process may conclude about where it stands, it concludes from what was
|
|
239
|
+
// persisted — see `markCaughtUpOnResume`, which the hold leaves alone.
|
|
177
240
|
let markCaughtUpIfSettled = (crossChainState: t) =>
|
|
178
|
-
if crossChainState->isSettledAtHead {
|
|
241
|
+
if !crossChainState.holdRealtime && crossChainState->isSettledAtHead {
|
|
179
242
|
crossChainState.isCaughtUp = true
|
|
180
243
|
}
|
|
181
244
|
|
|
@@ -206,13 +269,48 @@ let markCaughtUpOnResume = (crossChainState: t) => {
|
|
|
206
269
|
// and switches the indexer to realtime.
|
|
207
270
|
let markReady = (crossChainState: t, ~readyAt) => {
|
|
208
271
|
for i in 0 to crossChainState.chainIds->Array.length - 1 {
|
|
209
|
-
crossChainState
|
|
210
|
-
->
|
|
211
|
-
->ChainState.markReady(~readyAt)
|
|
272
|
+
let cs = crossChainState->getChainState(crossChainState.chainIds->Array.getUnsafe(i))
|
|
273
|
+
let wasReady = cs->ChainState.isReady
|
|
274
|
+
cs->ChainState.markReady(~readyAt)
|
|
275
|
+
|
|
276
|
+
// One line per chain, because `ready_at` is one column per chain: what the
|
|
277
|
+
// log says and what a reader finds in the row are the same fact.
|
|
278
|
+
if !wasReady {
|
|
279
|
+
cs
|
|
280
|
+
->ChainState.logger
|
|
281
|
+
->Logging.childInfo("Ready. Fully indexed for queries.")
|
|
282
|
+
}
|
|
212
283
|
}
|
|
213
284
|
crossChainState.isRealtime = true
|
|
214
285
|
}
|
|
215
286
|
|
|
287
|
+
// Each chain that has just finished indexing, said once, by the chain it is
|
|
288
|
+
// about — so a chain that finishes early says so then, rather than when the
|
|
289
|
+
// last chain in its process catches up.
|
|
290
|
+
let reportFinished = (crossChainState: t) => {
|
|
291
|
+
let waitingOnOthers = crossChainState.holdRealtime || crossChainState.chainIds->Array.length > 1
|
|
292
|
+
crossChainState.chainStates
|
|
293
|
+
->Dict.valuesToArray
|
|
294
|
+
->Array.forEach(cs =>
|
|
295
|
+
switch cs->ChainState.takeFinished {
|
|
296
|
+
| Some(EndBlock(block)) =>
|
|
297
|
+
cs
|
|
298
|
+
->ChainState.logger
|
|
299
|
+
->Logging.childInfo({"msg": "Indexed to the end block. This chain is done.", "block": block})
|
|
300
|
+
| Some(Backfill(block)) =>
|
|
301
|
+
cs
|
|
302
|
+
->ChainState.logger
|
|
303
|
+
->Logging.childInfo({
|
|
304
|
+
"msg": waitingOnOthers
|
|
305
|
+
? "Finished backfill. Waiting for the other chains."
|
|
306
|
+
: "Finished backfill.",
|
|
307
|
+
"block": block,
|
|
308
|
+
})
|
|
309
|
+
| None => ()
|
|
310
|
+
}
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
|
|
216
314
|
// --- Fetch control. ---
|
|
217
315
|
|
|
218
316
|
// Chains ordered furthest-behind first by fetch-frontier progress, so the
|
|
@@ -20,19 +20,41 @@ function calculateTargetBufferSize() {
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
function make(chainStates, isRealtime, targetBufferSizeOpt) {
|
|
23
|
+
function make(chainStates, isRealtime, targetBufferSizeOpt, holdRealtimeOpt) {
|
|
24
24
|
let targetBufferSize = targetBufferSizeOpt !== undefined ? targetBufferSizeOpt : (
|
|
25
25
|
Env.targetBufferSize !== undefined ? Env.targetBufferSize : 100000
|
|
26
26
|
);
|
|
27
|
+
let holdRealtime = holdRealtimeOpt !== undefined ? holdRealtimeOpt : false;
|
|
27
28
|
return {
|
|
28
29
|
chainStates: chainStates,
|
|
29
30
|
chainIds: Object.values(chainStates).map(cs => ChainState.chainConfig(cs).id),
|
|
30
31
|
isRealtime: isRealtime,
|
|
31
32
|
isCaughtUp: isRealtime,
|
|
32
|
-
targetBufferSize: targetBufferSize
|
|
33
|
+
targetBufferSize: targetBufferSize,
|
|
34
|
+
holdRealtime: holdRealtime
|
|
33
35
|
};
|
|
34
36
|
}
|
|
35
37
|
|
|
38
|
+
function releaseRealtime(crossChainState) {
|
|
39
|
+
crossChainState.holdRealtime = false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isHoldingRealtime(crossChainState) {
|
|
43
|
+
return crossChainState.holdRealtime;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function hasArrivedAtHead(crossChainState) {
|
|
47
|
+
if (crossChainState.isCaughtUp || crossChainState.isRealtime) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
let chainStates = Object.values(crossChainState.chainStates);
|
|
51
|
+
if (Utils.$$Array.notEmpty(chainStates)) {
|
|
52
|
+
return chainStates.every(ChainState.isReadyToEnterReorgThreshold);
|
|
53
|
+
} else {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
36
58
|
function chainStates(crossChainState) {
|
|
37
59
|
return crossChainState.chainStates;
|
|
38
60
|
}
|
|
@@ -94,10 +116,22 @@ function createBatch(crossChainState, config, frontier, batchSizeTarget) {
|
|
|
94
116
|
return Batch.make(config.checkpointSequence, frontier, Utils.Dict.mapValues(crossChainState.chainStates, none => ChainState.toChainBeforeBatch(none, crossChainState.isRealtime)), batchSizeTarget, HistoryPolicy.decide(config, shouldSaveHistory(crossChainState)));
|
|
95
117
|
}
|
|
96
118
|
|
|
119
|
+
function isReadyToEnterReorgThreshold(crossChainState, batch) {
|
|
120
|
+
if (crossChainState.holdRealtime) {
|
|
121
|
+
return false;
|
|
122
|
+
} else {
|
|
123
|
+
return Object.values(crossChainState.chainStates).every(cs => ChainState.isReadyToEnterReorgThresholdAfterBatch(cs, batch));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
97
127
|
function enterReorgThreshold(crossChainState) {
|
|
98
|
-
Logging.info("Reorg threshold reached");
|
|
99
128
|
for (let i = 0, i_finish = crossChainState.chainIds.length; i < i_finish; ++i) {
|
|
100
|
-
|
|
129
|
+
let cs = crossChainState.chainStates[crossChainState.chainIds[i]];
|
|
130
|
+
let liftsCeiling = ChainState.reorgThresholdLiftsCeiling(cs);
|
|
131
|
+
ChainState.enterReorgThreshold(cs);
|
|
132
|
+
if (liftsCeiling) {
|
|
133
|
+
Logging.childInfo(ChainState.logger(cs), ChainState.reorgThresholdEntryMessage);
|
|
134
|
+
}
|
|
101
135
|
}
|
|
102
136
|
}
|
|
103
137
|
|
|
@@ -111,7 +145,7 @@ function applyBatchProgress(crossChainState, batch, blockTimestampName) {
|
|
|
111
145
|
everyChainCaughtUp = false;
|
|
112
146
|
}
|
|
113
147
|
}
|
|
114
|
-
crossChainState.isCaughtUp = crossChainState.isCaughtUp || nextItemIsNone(crossChainState) && everyChainCaughtUp;
|
|
148
|
+
crossChainState.isCaughtUp = crossChainState.isCaughtUp || !crossChainState.holdRealtime && nextItemIsNone(crossChainState) && everyChainCaughtUp;
|
|
115
149
|
}
|
|
116
150
|
|
|
117
151
|
function isSettledAtHead(crossChainState) {
|
|
@@ -125,7 +159,7 @@ function isSettledAtHead(crossChainState) {
|
|
|
125
159
|
}
|
|
126
160
|
|
|
127
161
|
function markCaughtUpIfSettled(crossChainState) {
|
|
128
|
-
if (isSettledAtHead(crossChainState)) {
|
|
162
|
+
if (!crossChainState.holdRealtime && isSettledAtHead(crossChainState)) {
|
|
129
163
|
crossChainState.isCaughtUp = true;
|
|
130
164
|
return;
|
|
131
165
|
}
|
|
@@ -147,11 +181,36 @@ function markCaughtUpOnResume(crossChainState) {
|
|
|
147
181
|
|
|
148
182
|
function markReady(crossChainState, readyAt) {
|
|
149
183
|
for (let i = 0, i_finish = crossChainState.chainIds.length; i < i_finish; ++i) {
|
|
150
|
-
|
|
184
|
+
let cs = crossChainState.chainStates[crossChainState.chainIds[i]];
|
|
185
|
+
let wasReady = ChainState.isReady(cs);
|
|
186
|
+
ChainState.markReady(cs, readyAt);
|
|
187
|
+
if (!wasReady) {
|
|
188
|
+
Logging.childInfo(ChainState.logger(cs), "Ready. Fully indexed for queries.");
|
|
189
|
+
}
|
|
151
190
|
}
|
|
152
191
|
crossChainState.isRealtime = true;
|
|
153
192
|
}
|
|
154
193
|
|
|
194
|
+
function reportFinished(crossChainState) {
|
|
195
|
+
let waitingOnOthers = crossChainState.holdRealtime || crossChainState.chainIds.length > 1;
|
|
196
|
+
Object.values(crossChainState.chainStates).forEach(cs => {
|
|
197
|
+
let match = ChainState.takeFinished(cs);
|
|
198
|
+
if (match !== undefined) {
|
|
199
|
+
if (match.TAG === "EndBlock") {
|
|
200
|
+
return Logging.childInfo(ChainState.logger(cs), {
|
|
201
|
+
msg: "Indexed to the end block. This chain is done.",
|
|
202
|
+
block: match._0
|
|
203
|
+
});
|
|
204
|
+
} else {
|
|
205
|
+
return Logging.childInfo(ChainState.logger(cs), {
|
|
206
|
+
msg: waitingOnOthers ? "Finished backfill. Waiting for the other chains." : "Finished backfill.",
|
|
207
|
+
block: match._0
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
155
214
|
function priorityOrder(crossChainState) {
|
|
156
215
|
return Object.values(crossChainState.chainStates).toSorted((a, b) => Primitive_float.compare(ChainState.frontierProgress(a), ChainState.frontierProgress(b)));
|
|
157
216
|
}
|
|
@@ -256,12 +315,17 @@ export {
|
|
|
256
315
|
shouldSaveHistory,
|
|
257
316
|
nextItemIsNone,
|
|
258
317
|
getSafeCheckpointIdByChain,
|
|
318
|
+
releaseRealtime,
|
|
319
|
+
isHoldingRealtime,
|
|
320
|
+
hasArrivedAtHead,
|
|
321
|
+
isReadyToEnterReorgThreshold,
|
|
259
322
|
createBatch,
|
|
260
323
|
enterReorgThreshold,
|
|
261
324
|
applyBatchProgress,
|
|
262
325
|
markCaughtUpIfSettled,
|
|
263
326
|
markCaughtUpOnResume,
|
|
264
327
|
markReady,
|
|
328
|
+
reportFinished,
|
|
265
329
|
priorityOrder,
|
|
266
330
|
checkAndFetch,
|
|
267
331
|
}
|
package/src/CrossChainState.resi
CHANGED
|
@@ -5,7 +5,12 @@ type t
|
|
|
5
5
|
|
|
6
6
|
let calculateTargetBufferSize: unit => int
|
|
7
7
|
|
|
8
|
-
let make: (
|
|
8
|
+
let make: (
|
|
9
|
+
~chainStates: dict<ChainState.t>,
|
|
10
|
+
~isRealtime: bool,
|
|
11
|
+
~targetBufferSize: int=?,
|
|
12
|
+
~holdRealtime: bool=?,
|
|
13
|
+
) => t
|
|
9
14
|
|
|
10
15
|
// Accessors.
|
|
11
16
|
let chainStates: t => dict<ChainState.t>
|
|
@@ -25,12 +30,17 @@ let getSafeCheckpointIdByChain: (
|
|
|
25
30
|
) => array<(ChainId.t, option<Internal.checkpointId>)>
|
|
26
31
|
|
|
27
32
|
// Cross-chain transitions.
|
|
33
|
+
let releaseRealtime: t => unit
|
|
34
|
+
let isHoldingRealtime: t => bool
|
|
35
|
+
let hasArrivedAtHead: t => bool
|
|
36
|
+
let isReadyToEnterReorgThreshold: (t, ~batch: Batch.t) => bool
|
|
28
37
|
let createBatch: (t, ~config: Config.t, ~frontier: Frontier.t, ~batchSizeTarget: int) => Batch.t
|
|
29
38
|
let enterReorgThreshold: t => unit
|
|
30
39
|
let applyBatchProgress: (t, ~batch: Batch.t, ~blockTimestampName: string) => unit
|
|
31
40
|
let markCaughtUpIfSettled: t => unit
|
|
32
41
|
let markCaughtUpOnResume: t => unit
|
|
33
42
|
let markReady: (t, ~readyAt: Date.t) => unit
|
|
43
|
+
let reportFinished: t => unit
|
|
34
44
|
|
|
35
45
|
// Fetch control.
|
|
36
46
|
let priorityOrder: t => array<ChainState.t>
|
package/src/Env.res
CHANGED
|
@@ -127,6 +127,10 @@ module Db = {
|
|
|
127
127
|
//the SSL modes should be provided as string otherwise as 'require' | 'allow' | 'prefer' | 'verify-full'
|
|
128
128
|
~devFallback=Bool(false),
|
|
129
129
|
)
|
|
130
|
+
// The budget for the whole run, not for one process: a run that splits across
|
|
131
|
+
// workers divides it among them, and each caps its own pool to its share.
|
|
132
|
+
// Splitting takes two workers and a worker takes two connections, so a budget
|
|
133
|
+
// under 4 — the default among them — keeps the run in one process.
|
|
130
134
|
let maxConnections = envSafe->EnvSafe.get("ENVIO_PG_MAX_CONNECTIONS", S.int, ~fallback=2)
|
|
131
135
|
}
|
|
132
136
|
|
package/src/ErrorHandling.res
CHANGED
|
@@ -11,14 +11,25 @@ let log = (self: t) => {
|
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
// The exception to hand to whoever is outside the indexer. A failure raised
|
|
15
|
+
// from a message alone carries no exception of its own, so rethrowing or
|
|
16
|
+
// rejecting with `exn` would hand over a bare `null` and the reason would live
|
|
17
|
+
// only in the logs. Fall back to the message, which is the whole of what such a
|
|
18
|
+
// failure knows.
|
|
19
|
+
let toExn = (self: t) =>
|
|
20
|
+
switch self.exn->(Utils.magic: exn => Nullable.t<exn>)->Nullable.toOption {
|
|
21
|
+
| Some(exn) => exn->Utils.prettifyExn
|
|
22
|
+
| None => Utils.Error.make(self.msg->Option.getOr("Indexer has failed with an unexpected error"))
|
|
23
|
+
}
|
|
24
|
+
|
|
14
25
|
let raiseExn = (self: t) => {
|
|
15
|
-
self
|
|
26
|
+
self->toExn->throw
|
|
16
27
|
}
|
|
17
28
|
|
|
18
29
|
let mkLogAndRaise = (~logger=?, ~msg=?, exn) => {
|
|
19
|
-
let
|
|
20
|
-
|
|
21
|
-
|
|
30
|
+
let self = exn->Utils.prettifyExn->make(~logger?, ~msg?)
|
|
31
|
+
self->log
|
|
32
|
+
self->raiseExn
|
|
22
33
|
}
|
|
23
34
|
|
|
24
35
|
let unwrapLogAndRaise = (~logger=?, ~msg=?, result) => {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as Utils from "./Utils.res.mjs";
|
|
4
4
|
import * as Logging from "./Logging.res.mjs";
|
|
5
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
6
|
|
|
6
7
|
function make(exn, loggerOpt, msg) {
|
|
7
8
|
let logger = loggerOpt !== undefined ? loggerOpt : Logging.getLogger();
|
|
@@ -23,14 +24,23 @@ function log(self) {
|
|
|
23
24
|
}
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
function toExn(self) {
|
|
28
|
+
let exn = self.exn;
|
|
29
|
+
if (exn == null) {
|
|
30
|
+
return new Error(Stdlib_Option.getOr(self.msg, "Indexer has failed with an unexpected error"));
|
|
31
|
+
} else {
|
|
32
|
+
return Utils.prettifyExn(exn);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
26
36
|
function raiseExn(self) {
|
|
27
|
-
throw
|
|
37
|
+
throw toExn(self);
|
|
28
38
|
}
|
|
29
39
|
|
|
30
40
|
function mkLogAndRaise(logger, msg, exn) {
|
|
31
|
-
let
|
|
32
|
-
log(
|
|
33
|
-
throw
|
|
41
|
+
let self = make(Utils.prettifyExn(exn), logger, msg);
|
|
42
|
+
log(self);
|
|
43
|
+
throw toExn(self);
|
|
34
44
|
}
|
|
35
45
|
|
|
36
46
|
function unwrapLogAndRaise(logger, msg, result) {
|
|
@@ -43,12 +53,13 @@ function unwrapLogAndRaise(logger, msg, result) {
|
|
|
43
53
|
|
|
44
54
|
function logAndRaise(self) {
|
|
45
55
|
log(self);
|
|
46
|
-
throw
|
|
56
|
+
throw toExn(self);
|
|
47
57
|
}
|
|
48
58
|
|
|
49
59
|
export {
|
|
50
60
|
make,
|
|
51
61
|
log,
|
|
62
|
+
toExn,
|
|
52
63
|
raiseExn,
|
|
53
64
|
mkLogAndRaise,
|
|
54
65
|
unwrapLogAndRaise,
|
package/src/FinalizeBackfill.res
CHANGED
|
@@ -11,9 +11,15 @@
|
|
|
11
11
|
// `envio start --chain` process is indexing and never waits on one.
|
|
12
12
|
|
|
13
13
|
let runOnce = async (state: IndexerState.t) => {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
// Said by the process rather than by each of its chains: the indexes are one
|
|
15
|
+
// build over the tables, and the pause is the whole process's. A chain has
|
|
16
|
+
// already said it caught up, and says it is ready once this commits. The
|
|
17
|
+
// chains are named because the pause is theirs, and a split run has a process
|
|
18
|
+
// saying this for each part of it.
|
|
19
|
+
Logging.info({
|
|
20
|
+
"msg": "Building database indexes. Indexing is paused until they are ready, which can take a while on a large database.",
|
|
21
|
+
"chainIds": state->IndexerState.crossChainState->CrossChainState.chainIds,
|
|
22
|
+
})
|
|
17
23
|
|
|
18
24
|
await Writing.flush(state)
|
|
19
25
|
|
|
@@ -32,8 +38,8 @@ let runOnce = async (state: IndexerState.t) => {
|
|
|
32
38
|
|
|
33
39
|
// Only after the commit: in-memory readiness must never run ahead of the
|
|
34
40
|
// `ready_at` a restart would read back.
|
|
41
|
+
// Says so per chain, which is the grain `ready_at` is committed at.
|
|
35
42
|
state->IndexerState.markReady(~readyAt)
|
|
36
|
-
Logging.info("The indexer is ready. Switching to realtime indexing.")
|
|
37
43
|
}
|
|
38
44
|
}
|
|
39
45
|
|
|
@@ -7,7 +7,10 @@ import * as IndexerState from "./IndexerState.res.mjs";
|
|
|
7
7
|
import * as CrossChainState from "./CrossChainState.res.mjs";
|
|
8
8
|
|
|
9
9
|
async function runOnce(state) {
|
|
10
|
-
Logging.info(
|
|
10
|
+
Logging.info({
|
|
11
|
+
msg: "Building database indexes. Indexing is paused until they are ready, which can take a while on a large database.",
|
|
12
|
+
chainIds: CrossChainState.chainIds(IndexerState.crossChainState(state))
|
|
13
|
+
});
|
|
11
14
|
await Writing.flush(state);
|
|
12
15
|
if (IndexerState.hasFailedWrite(state)) {
|
|
13
16
|
return;
|
|
@@ -16,8 +19,7 @@ async function runOnce(state) {
|
|
|
16
19
|
let storage = Persistence.getInitializedStorageOrThrow(persistence);
|
|
17
20
|
let readyAt = new Date();
|
|
18
21
|
await storage.finalizeBackfill(persistence.allEntities, CrossChainState.chainIds(IndexerState.crossChainState(state)), readyAt);
|
|
19
|
-
IndexerState.markReady(state, readyAt);
|
|
20
|
-
return Logging.info("The indexer is ready. Switching to realtime indexing.");
|
|
22
|
+
return IndexerState.markReady(state, readyAt);
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
function run(state) {
|
package/src/HandlerRegister.res
CHANGED
|
@@ -493,41 +493,6 @@ let getSimulateOnEventRegistrations = (
|
|
|
493
493
|
}
|
|
494
494
|
}
|
|
495
495
|
|
|
496
|
-
// An RPC source can only deliver the fields it knows how to parse; the rest are
|
|
497
|
-
// silently skipped at materialisation. Every RPC on the chain counts, whatever
|
|
498
|
-
// it's for — a fallback or realtime source runs the same parsers as a sync one,
|
|
499
|
-
// so a field it can't deliver would go missing for whichever blocks it served.
|
|
500
|
-
// The selection can come from either `config.yaml` or the inline `fields`
|
|
501
|
-
// option, so the message names neither. Runs on the registrations a chain
|
|
502
|
-
// actually keeps, so a handler whose `where` opts out of this chain isn't held
|
|
503
|
-
// to its limits.
|
|
504
|
-
//
|
|
505
|
-
// The `config.yaml` half of this is also rejected at codegen, by the
|
|
506
|
-
// `RpcTransactionField` subenum in `system_config.rs`. That one reports every
|
|
507
|
-
// offending field at once and before the project builds; this one is the only
|
|
508
|
-
// check an inline selection reaches. `RpcFieldSelection_test.res` pins the two
|
|
509
|
-
// to the same field set.
|
|
510
|
-
let validateRpcFieldSelection = (
|
|
511
|
-
chainConfig: Config.chain,
|
|
512
|
-
registrations: array<Internal.onEventRegistration>,
|
|
513
|
-
) => {
|
|
514
|
-
let hasRpc = switch chainConfig.sourceConfig {
|
|
515
|
-
| EvmSourceConfig({rpcs}) => !(rpcs->Utils.Array.isEmpty)
|
|
516
|
-
| _ => false
|
|
517
|
-
}
|
|
518
|
-
if hasRpc {
|
|
519
|
-
registrations->Array.forEach(reg =>
|
|
520
|
-
reg.fieldSelection.transactionFields->Utils.Set.forEach(name =>
|
|
521
|
-
if !RpcSource.isRpcTransactionField(name) {
|
|
522
|
-
JsError.throwWithMessage(
|
|
523
|
-
`The "${name}" transaction field selected for the "${reg.eventConfig.name}" event on contract "${reg.eventConfig.contractName}" is unavailable for indexing via RPC. Remove it from the field selection, or remove chain ${chainConfig.id->ChainId.toString}'s RPC source — even an RPC the chain only falls back to has to deliver the selection.`,
|
|
524
|
-
)
|
|
525
|
-
}
|
|
526
|
-
)
|
|
527
|
-
)
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
|
|
531
496
|
let finishRegistration = (~config: Config.t): registrationsByChainId => {
|
|
532
497
|
switch getActiveRegistration() {
|
|
533
498
|
| Some(r) => {
|
|
@@ -614,8 +579,6 @@ let finishRegistration = (~config: Config.t): registrationsByChainId => {
|
|
|
614
579
|
}
|
|
615
580
|
})
|
|
616
581
|
|
|
617
|
-
validateRpcFieldSelection(chainConfig, onEventRegistrations)
|
|
618
|
-
|
|
619
582
|
registrationsByChainId->Dict.set(
|
|
620
583
|
key,
|
|
621
584
|
{
|
|
@@ -5,7 +5,6 @@ import * as ChainId from "./ChainId.res.mjs";
|
|
|
5
5
|
import * as Logging from "./Logging.res.mjs";
|
|
6
6
|
import * as ChainMap from "./ChainMap.res.mjs";
|
|
7
7
|
import * as Internal from "./Internal.res.mjs";
|
|
8
|
-
import * as RpcSource from "./sources/RpcSource.res.mjs";
|
|
9
8
|
import * as EnvioGlobal from "./EnvioGlobal.res.mjs";
|
|
10
9
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
11
10
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
@@ -327,22 +326,6 @@ function getSimulateOnEventRegistrations(config, chainId, eventConfig) {
|
|
|
327
326
|
}
|
|
328
327
|
}
|
|
329
328
|
|
|
330
|
-
function validateRpcFieldSelection(chainConfig, registrations) {
|
|
331
|
-
let match = chainConfig.sourceConfig;
|
|
332
|
-
let hasRpc;
|
|
333
|
-
hasRpc = match.TAG === "EvmSourceConfig" ? !Utils.$$Array.isEmpty(match.rpcs) : false;
|
|
334
|
-
if (hasRpc) {
|
|
335
|
-
registrations.forEach(reg => {
|
|
336
|
-
reg.fieldSelection.transactionFields.forEach(name => {
|
|
337
|
-
if (!RpcSource.isRpcTransactionField(name)) {
|
|
338
|
-
return Stdlib_JsError.throwWithMessage(`The "` + name + `" transaction field selected for the "` + reg.eventConfig.name + `" event on contract "` + reg.eventConfig.contractName + `" is unavailable for indexing via RPC. Remove it from the field selection, or remove chain ` + ChainId.toString(chainConfig.id) + `'s RPC source — even an RPC the chain only falls back to has to deliver the selection.`);
|
|
339
|
-
}
|
|
340
|
-
});
|
|
341
|
-
});
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
329
|
function finishRegistration(config) {
|
|
347
330
|
let r = EnvioGlobal.value.activeRegistration;
|
|
348
331
|
if (r === undefined) {
|
|
@@ -389,7 +372,6 @@ function finishRegistration(config) {
|
|
|
389
372
|
let newrecord = {...reg};
|
|
390
373
|
onEventRegistrations.push((newrecord.index = onEventRegistrations.length, newrecord));
|
|
391
374
|
});
|
|
392
|
-
validateRpcFieldSelection(chainConfig, onEventRegistrations);
|
|
393
375
|
let chainRegs = r.registrationsByChainId[key];
|
|
394
376
|
registrationsByChainId[key] = {
|
|
395
377
|
onEventRegistrations: onEventRegistrations,
|