envio 3.2.1 → 3.3.0-alpha.1
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 -7
- package/src/Batch.res +12 -15
- package/src/Batch.res.mjs +4 -5
- package/src/BatchProcessing.res +199 -0
- package/src/BatchProcessing.res.mjs +125 -0
- package/src/ChainFetching.res +373 -0
- package/src/ChainFetching.res.mjs +206 -0
- package/src/ChainMetadata.res +27 -0
- package/src/ChainMetadata.res.mjs +27 -0
- package/src/ChainState.res +643 -0
- package/src/ChainState.res.mjs +476 -0
- package/src/ChainState.resi +87 -0
- package/src/Config.res +13 -4
- package/src/Config.res.mjs +8 -3
- package/src/ContractRegisterContext.res +106 -0
- package/src/ContractRegisterContext.res.mjs +76 -0
- package/src/Core.res +3 -0
- package/src/CrossChainState.res +294 -0
- package/src/CrossChainState.res.mjs +221 -0
- package/src/CrossChainState.resi +39 -0
- package/src/Ecosystem.res +58 -0
- package/src/Ecosystem.res.mjs +43 -1
- package/src/Env.res +0 -2
- package/src/Env.res.mjs +0 -3
- package/src/EventConfigBuilder.res +2 -2
- package/src/EventProcessing.res +60 -46
- package/src/EventProcessing.res.mjs +33 -32
- package/src/EventUtils.res +0 -4
- package/src/EventUtils.res.mjs +0 -4
- package/src/ExitOnCaughtUp.res +10 -0
- package/src/ExitOnCaughtUp.res.mjs +22 -0
- package/src/FetchState.res +205 -73
- package/src/FetchState.res.mjs +242 -103
- package/src/InMemoryStore.res +36 -451
- package/src/InMemoryStore.res.mjs +20 -369
- package/src/IndexerLoop.res +43 -0
- package/src/IndexerLoop.res.mjs +46 -0
- package/src/IndexerState.res +536 -0
- package/src/IndexerState.res.mjs +543 -0
- package/src/IndexerState.resi +128 -0
- package/src/Internal.res +29 -8
- package/src/LoadLayer.res +20 -18
- package/src/LoadLayer.res.mjs +14 -12
- package/src/LoadLayer.resi +6 -3
- package/src/Logging.res +11 -44
- package/src/Logging.res.mjs +10 -42
- package/src/Main.res +54 -79
- package/src/Main.res.mjs +44 -56
- package/src/PgStorage.res +8 -71
- package/src/PgStorage.res.mjs +3 -47
- package/src/Prometheus.res +1 -13
- package/src/Prometheus.res.mjs +84 -96
- package/src/PruneStaleHistory.res +45 -0
- package/src/PruneStaleHistory.res.mjs +46 -0
- package/src/RawEvent.res +73 -0
- package/src/RawEvent.res.mjs +51 -0
- package/src/Rollback.res +204 -0
- package/src/Rollback.res.mjs +118 -0
- package/src/SimulateItems.res +12 -10
- package/src/SimulateItems.res.mjs +1 -1
- package/src/UserContext.res +26 -114
- package/src/UserContext.res.mjs +15 -78
- package/src/Writing.res +242 -0
- package/src/Writing.res.mjs +215 -0
- package/src/db/InternalTable.res +14 -27
- package/src/sources/Evm.res +40 -1
- package/src/sources/Evm.res.mjs +94 -84
- package/src/sources/Fuel.res +40 -1
- package/src/sources/Fuel.res.mjs +36 -26
- package/src/sources/HyperFuel.res +41 -120
- package/src/sources/HyperFuel.res.mjs +42 -80
- package/src/sources/HyperFuel.resi +1 -24
- package/src/sources/HyperFuelClient.res +16 -297
- package/src/sources/HyperFuelClient.res.mjs +5 -4
- package/src/sources/HyperFuelSource.res +33 -8
- package/src/sources/HyperFuelSource.res.mjs +56 -10
- package/src/sources/HyperSyncSource.res +5 -3
- package/src/sources/HyperSyncSource.res.mjs +1 -1
- package/src/sources/RpcSource.res +2 -2
- package/src/sources/RpcSource.res.mjs +1 -1
- package/src/sources/SourceManager.res +14 -23
- package/src/sources/SourceManager.res.mjs +27 -29
- package/src/sources/SourceManager.resi +4 -2
- package/src/sources/Svm.res +18 -1
- package/src/sources/Svm.res.mjs +30 -22
- package/src/sources/SvmHyperSyncSource.res +1 -1
- package/src/sources/SvmHyperSyncSource.res.mjs +1 -1
- package/src/tui/Tui.res +38 -36
- package/src/tui/Tui.res.mjs +51 -51
- package/src/ChainFetcher.res +0 -519
- package/src/ChainFetcher.res.mjs +0 -314
- package/src/ChainManager.res +0 -358
- package/src/ChainManager.res.mjs +0 -291
- package/src/Ctx.res +0 -6
- package/src/Ctx.res.mjs +0 -2
- package/src/GlobalState.res +0 -1056
- package/src/GlobalState.res.mjs +0 -1086
- package/src/GlobalStateManager.res +0 -57
- package/src/GlobalStateManager.res.mjs +0 -68
- package/src/GlobalStateManager.resi +0 -7
- package/src/Ports.res +0 -5
- package/src/Ports.res.mjs +0 -9
- package/src/adapters/MarkBatchProcessedAdapter.res +0 -5
- package/src/adapters/MarkBatchProcessedAdapter.res.mjs +0 -14
- package/src/sources/EnvioApiClient.res +0 -15
- package/src/sources/EnvioApiClient.res.mjs +0 -24
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// The contractRegister handler context: context.chain.ContractName.add(address).
|
|
2
|
+
// Independent of the in-memory store, so it stays off IndexerState and the
|
|
3
|
+
// fetch-time contract registration in ChainState doesn't pull the state in.
|
|
4
|
+
|
|
5
|
+
type contractRegisterParams = {
|
|
6
|
+
item: Internal.item,
|
|
7
|
+
onRegister: (~item: Internal.item, ~contractAddress: Address.t, ~contractName: string) => unit,
|
|
8
|
+
config: Config.t,
|
|
9
|
+
mutable isResolved: bool,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Helper to create a validated add function for contract registration.
|
|
13
|
+
// The isResolved check has to live inside the returned closure (not just in the
|
|
14
|
+
// outer proxy trap) because users can capture `const add = context.chain.X.add`
|
|
15
|
+
// before awaiting — a later call would otherwise bypass the resolved guard.
|
|
16
|
+
let makeAddFunction = (~params: contractRegisterParams, ~contractName: string): (
|
|
17
|
+
Address.t => unit
|
|
18
|
+
) => {
|
|
19
|
+
(contractAddress: Address.t) => {
|
|
20
|
+
if params.isResolved {
|
|
21
|
+
Utils.Error.make(`Impossible to access context.chain after the contract register is resolved. Make sure you didn't miss an await in the handler.`)->ErrorHandling.mkLogAndRaise(
|
|
22
|
+
~logger=Ecosystem.getItemLogger(params.item, ~ecosystem=params.config.ecosystem),
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
let validatedAddress = if params.config.ecosystem.name === Evm {
|
|
26
|
+
// The value is passed from the user-land,
|
|
27
|
+
// so we need to validate and checksum/lowercase the address.
|
|
28
|
+
if params.config.lowercaseAddresses {
|
|
29
|
+
contractAddress->Address.Evm.fromAddressLowercaseOrThrow
|
|
30
|
+
} else {
|
|
31
|
+
contractAddress->Address.Evm.fromAddressOrThrow
|
|
32
|
+
}
|
|
33
|
+
} else {
|
|
34
|
+
// TODO: Ideally we should do the same for other ecosystems
|
|
35
|
+
contractAddress
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
params.onRegister(~item=params.item, ~contractAddress=validatedAddress, ~contractName)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Chain proxy for contractRegister context: context.chain.ContractName.add(address)
|
|
43
|
+
let contractRegisterChainTraps: Utils.Proxy.traps<contractRegisterParams> = {
|
|
44
|
+
get: (~target as params, ~prop: unknown) => {
|
|
45
|
+
let prop = prop->(Utils.magic: unknown => string)
|
|
46
|
+
switch prop {
|
|
47
|
+
| "id" =>
|
|
48
|
+
let eventItem = params.item->Internal.castUnsafeEventItem
|
|
49
|
+
eventItem.chain->ChainMap.Chain.toChainId->(Utils.magic: int => unknown)
|
|
50
|
+
| _ =>
|
|
51
|
+
// Look up the contract name directly in config contracts across all chains.
|
|
52
|
+
let contractName = prop
|
|
53
|
+
let isValidContract =
|
|
54
|
+
params.config.chainMap
|
|
55
|
+
->ChainMap.values
|
|
56
|
+
->Array.some(chain => chain.contracts->Array.some(c => c.name === contractName))
|
|
57
|
+
if isValidContract {
|
|
58
|
+
let addFn = makeAddFunction(~params, ~contractName)
|
|
59
|
+
{"add": addFn}->(Utils.magic: {"add": Address.t => unit} => unknown)
|
|
60
|
+
} else {
|
|
61
|
+
JsError.throwWithMessage(
|
|
62
|
+
`Invalid contract name '${prop}' on context.chain. ${EntityFilter.codegenHelpMessage}`,
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let contractRegisterTraps: Utils.Proxy.traps<contractRegisterParams> = {
|
|
70
|
+
get: (~target as params, ~prop: unknown) => {
|
|
71
|
+
let prop = prop->(Utils.magic: unknown => string)
|
|
72
|
+
if params.isResolved {
|
|
73
|
+
Utils.Error.make(
|
|
74
|
+
`Impossible to access context.${prop} after the contract register is resolved. Make sure you didn't miss an await in the handler.`,
|
|
75
|
+
)->ErrorHandling.mkLogAndRaise(
|
|
76
|
+
~logger=Ecosystem.getItemLogger(params.item, ~ecosystem=params.config.ecosystem),
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
switch prop {
|
|
80
|
+
| "log" =>
|
|
81
|
+
Ecosystem.getItemUserLogger(params.item, ~ecosystem=params.config.ecosystem)->(
|
|
82
|
+
Utils.magic: Envio.logger => unknown
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
| "chain" =>
|
|
86
|
+
params
|
|
87
|
+
->Utils.Proxy.make(contractRegisterChainTraps)
|
|
88
|
+
->(Utils.magic: contractRegisterParams => unknown)
|
|
89
|
+
| _ =>
|
|
90
|
+
JsError.throwWithMessage(
|
|
91
|
+
`Invalid context access by '${prop}' property. Use context.chain.ContractName.add(address) to register contracts. ${EntityFilter.codegenHelpMessage}`,
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let getContractRegisterContext = (params: contractRegisterParams) => {
|
|
98
|
+
params
|
|
99
|
+
->Utils.Proxy.make(contractRegisterTraps)
|
|
100
|
+
->(Utils.magic: contractRegisterParams => Internal.contractRegisterContext)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let getContractRegisterArgs = (params: contractRegisterParams): Internal.contractRegisterArgs => {
|
|
104
|
+
event: params.item->Ecosystem.getItemEvent(~ecosystem=params.config.ecosystem),
|
|
105
|
+
context: getContractRegisterContext(params),
|
|
106
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Address from "./Address.res.mjs";
|
|
4
|
+
import * as ChainMap from "./ChainMap.res.mjs";
|
|
5
|
+
import * as Ecosystem from "./Ecosystem.res.mjs";
|
|
6
|
+
import * as EntityFilter from "./db/EntityFilter.res.mjs";
|
|
7
|
+
import * as ErrorHandling from "./ErrorHandling.res.mjs";
|
|
8
|
+
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
9
|
+
|
|
10
|
+
function makeAddFunction(params, contractName) {
|
|
11
|
+
return contractAddress => {
|
|
12
|
+
if (params.isResolved) {
|
|
13
|
+
ErrorHandling.mkLogAndRaise(Ecosystem.getItemLogger(params.item, params.config.ecosystem), undefined, new Error(`Impossible to access context.chain after the contract register is resolved. Make sure you didn't miss an await in the handler.`));
|
|
14
|
+
}
|
|
15
|
+
let validatedAddress = params.config.ecosystem.name === "evm" ? (
|
|
16
|
+
params.config.lowercaseAddresses ? Address.Evm.fromAddressLowercaseOrThrow(contractAddress) : Address.Evm.fromAddressOrThrow(contractAddress)
|
|
17
|
+
) : contractAddress;
|
|
18
|
+
params.onRegister(params.item, validatedAddress, contractName);
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let contractRegisterChainTraps_get = (params, prop) => {
|
|
23
|
+
if (prop === "id") {
|
|
24
|
+
return params.item.chain;
|
|
25
|
+
}
|
|
26
|
+
let isValidContract = ChainMap.values(params.config.chainMap).some(chain => chain.contracts.some(c => c.name === prop));
|
|
27
|
+
if (isValidContract) {
|
|
28
|
+
return {
|
|
29
|
+
add: makeAddFunction(params, prop)
|
|
30
|
+
};
|
|
31
|
+
} else {
|
|
32
|
+
return Stdlib_JsError.throwWithMessage(`Invalid contract name '` + prop + `' on context.chain. ` + EntityFilter.codegenHelpMessage);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
let contractRegisterChainTraps = {
|
|
37
|
+
get: contractRegisterChainTraps_get
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
let contractRegisterTraps_get = (params, prop) => {
|
|
41
|
+
if (params.isResolved) {
|
|
42
|
+
ErrorHandling.mkLogAndRaise(Ecosystem.getItemLogger(params.item, params.config.ecosystem), undefined, new Error(`Impossible to access context.` + prop + ` after the contract register is resolved. Make sure you didn't miss an await in the handler.`));
|
|
43
|
+
}
|
|
44
|
+
switch (prop) {
|
|
45
|
+
case "chain" :
|
|
46
|
+
return new Proxy(params, contractRegisterChainTraps);
|
|
47
|
+
case "log" :
|
|
48
|
+
return Ecosystem.getItemUserLogger(params.item, params.config.ecosystem);
|
|
49
|
+
default:
|
|
50
|
+
return Stdlib_JsError.throwWithMessage(`Invalid context access by '` + prop + `' property. Use context.chain.ContractName.add(address) to register contracts. ` + EntityFilter.codegenHelpMessage);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
let contractRegisterTraps = {
|
|
55
|
+
get: contractRegisterTraps_get
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
function getContractRegisterContext(params) {
|
|
59
|
+
return new Proxy(params, contractRegisterTraps);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getContractRegisterArgs(params) {
|
|
63
|
+
return {
|
|
64
|
+
event: Ecosystem.getItemEvent(params.item, params.config.ecosystem),
|
|
65
|
+
context: new Proxy(params, contractRegisterTraps)
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export {
|
|
70
|
+
makeAddFunction,
|
|
71
|
+
contractRegisterChainTraps,
|
|
72
|
+
contractRegisterTraps,
|
|
73
|
+
getContractRegisterContext,
|
|
74
|
+
getContractRegisterArgs,
|
|
75
|
+
}
|
|
76
|
+
/* Address Not a pure module */
|
package/src/Core.res
CHANGED
|
@@ -8,6 +8,7 @@ type evmHypersyncClientCtor
|
|
|
8
8
|
type evmRpcClientCtor
|
|
9
9
|
type evmDecoderCtor
|
|
10
10
|
type svmHypersyncClientCtor
|
|
11
|
+
type hyperfuelClientCtor
|
|
11
12
|
|
|
12
13
|
type addon = {
|
|
13
14
|
getConfigJson: (~configPath: Null.t<string>, ~directory: Null.t<string>) => string,
|
|
@@ -20,6 +21,8 @@ type addon = {
|
|
|
20
21
|
evmDecoder: evmDecoderCtor,
|
|
21
22
|
@as("SvmHypersyncClient")
|
|
22
23
|
svmHypersyncClient: svmHypersyncClientCtor,
|
|
24
|
+
@as("HyperfuelClient")
|
|
25
|
+
hyperfuelClient: hyperfuelClientCtor,
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
@module("node:module") external createRequire: string => {..} = "createRequire"
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// Whole-indexer view over every chain's ChainState: the chain collection plus
|
|
2
|
+
// the run-wide flags derived from it. Owns the cross-chain reads and the
|
|
3
|
+
// batch/threshold transitions that touch every chain. `t` is mutated in place
|
|
4
|
+
// through the transitions below; the type is opaque in the interface.
|
|
5
|
+
|
|
6
|
+
type t = {
|
|
7
|
+
chainStates: dict<ChainState.t>,
|
|
8
|
+
// Chain ids in a stable order, so the cross-chain loops iterate the chains
|
|
9
|
+
// without allocating a values array on every tick.
|
|
10
|
+
chainIds: array<int>,
|
|
11
|
+
// True once every chain has caught up to head/endBlock. Monotonic during a run.
|
|
12
|
+
mutable isRealtime: bool,
|
|
13
|
+
mutable isInReorgThreshold: bool,
|
|
14
|
+
// Indexer-wide fetch buffer pool (item count), shared across all chains.
|
|
15
|
+
targetBufferSize: int,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// The whole-indexer fetch buffer pool, independent of chain count.
|
|
19
|
+
let calculateTargetBufferSize = () =>
|
|
20
|
+
switch Env.targetBufferSize {
|
|
21
|
+
| Some(size) => size
|
|
22
|
+
| None => 100_000
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let make = (
|
|
26
|
+
~chainStates,
|
|
27
|
+
~isInReorgThreshold,
|
|
28
|
+
~isRealtime,
|
|
29
|
+
~targetBufferSize=calculateTargetBufferSize(),
|
|
30
|
+
): t => {
|
|
31
|
+
let crossChainState = {
|
|
32
|
+
chainStates,
|
|
33
|
+
chainIds: chainStates->Dict.valuesToArray->Array.map(cs => (cs->ChainState.chainConfig).id),
|
|
34
|
+
isRealtime,
|
|
35
|
+
isInReorgThreshold,
|
|
36
|
+
targetBufferSize,
|
|
37
|
+
}
|
|
38
|
+
Prometheus.IndexingTargetBufferSize.set(~targetBufferSize)
|
|
39
|
+
crossChainState
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Resolve a chain's state by id. The id always comes from `chainIds`, which is
|
|
43
|
+
// derived from `chainStates`, so the entry is guaranteed present.
|
|
44
|
+
let getChainState = (crossChainState: t, chainId) =>
|
|
45
|
+
crossChainState.chainStates->Utils.Dict.dangerouslyGetByIntNonOption(chainId)->Option.getUnsafe
|
|
46
|
+
|
|
47
|
+
// --- Accessors. ---
|
|
48
|
+
|
|
49
|
+
let chainStates = (crossChainState: t) => crossChainState.chainStates
|
|
50
|
+
let isRealtime = (crossChainState: t) => crossChainState.isRealtime
|
|
51
|
+
let isInReorgThreshold = (crossChainState: t) => crossChainState.isInReorgThreshold
|
|
52
|
+
|
|
53
|
+
// Ready-to-process items across every chain — the live draw against
|
|
54
|
+
// targetBufferSize, which is a budget of processable events (items stuck behind
|
|
55
|
+
// a gap don't count toward the goal of keeping ~targetBufferSize ready).
|
|
56
|
+
let totalReadyCount = (crossChainState: t) => {
|
|
57
|
+
let total = ref(0)
|
|
58
|
+
for i in 0 to crossChainState.chainIds->Array.length - 1 {
|
|
59
|
+
let cs = crossChainState->getChainState(crossChainState.chainIds->Array.getUnsafe(i))
|
|
60
|
+
total := total.contents + cs->ChainState.fetchState->FetchState.bufferReadyCount
|
|
61
|
+
}
|
|
62
|
+
total.contents
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// --- Derived (pure). ---
|
|
66
|
+
|
|
67
|
+
let nextItemIsNone = (crossChainState: t): bool =>
|
|
68
|
+
!Batch.hasReadyItem(
|
|
69
|
+
crossChainState.chainStates->Dict.valuesToArray->Array.map(ChainState.fetchState),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
let getSafeCheckpointId = (crossChainState: t) => {
|
|
73
|
+
let result: ref<option<bigint>> = ref(None)
|
|
74
|
+
|
|
75
|
+
for i in 0 to crossChainState.chainIds->Array.length - 1 {
|
|
76
|
+
let cs = crossChainState->getChainState(crossChainState.chainIds->Array.getUnsafe(i))
|
|
77
|
+
switch cs->ChainState.safeCheckpointTracking {
|
|
78
|
+
| None => () // Skip chains with maxReorgDepth = 0
|
|
79
|
+
| Some(safeCheckpointTracking) =>
|
|
80
|
+
let safeCheckpointId =
|
|
81
|
+
safeCheckpointTracking->SafeCheckpointTracking.getSafeCheckpointId(
|
|
82
|
+
~sourceBlockNumber=(cs->ChainState.fetchState).knownHeight,
|
|
83
|
+
)
|
|
84
|
+
switch result.contents {
|
|
85
|
+
| None => result := Some(safeCheckpointId)
|
|
86
|
+
| Some(current) if safeCheckpointId < current => result := Some(safeCheckpointId)
|
|
87
|
+
| _ => ()
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
switch result.contents {
|
|
93
|
+
| Some(id) if id > 0n => Some(id)
|
|
94
|
+
| _ => None // No safe checkpoint found
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// --- Cross-chain transitions. ---
|
|
99
|
+
|
|
100
|
+
let createBatch = (
|
|
101
|
+
crossChainState: t,
|
|
102
|
+
~processedCheckpointId,
|
|
103
|
+
~batchSizeTarget: int,
|
|
104
|
+
~isRollback: bool,
|
|
105
|
+
): Batch.t => {
|
|
106
|
+
Batch.make(
|
|
107
|
+
~isInReorgThreshold=crossChainState.isInReorgThreshold,
|
|
108
|
+
~checkpointIdBeforeBatch=processedCheckpointId->BigInt.add(
|
|
109
|
+
// Since for rollback we have a diff checkpoint id.
|
|
110
|
+
// This is needed to currectly overwrite old state
|
|
111
|
+
// in an append-only ClickHouse insert.
|
|
112
|
+
isRollback ? 1n : 0n,
|
|
113
|
+
),
|
|
114
|
+
~chainsBeforeBatch=crossChainState.chainStates->Utils.Dict.mapValues((
|
|
115
|
+
cs
|
|
116
|
+
): Batch.chainBeforeBatch => {
|
|
117
|
+
fetchState: cs->ChainState.fetchState,
|
|
118
|
+
progressBlockNumber: cs->ChainState.committedProgressBlockNumber,
|
|
119
|
+
totalEventsProcessed: cs->ChainState.numEventsProcessed,
|
|
120
|
+
sourceBlockNumber: (cs->ChainState.fetchState).knownHeight,
|
|
121
|
+
reorgDetection: cs->ChainState.reorgDetection,
|
|
122
|
+
chainConfig: cs->ChainState.chainConfig,
|
|
123
|
+
}),
|
|
124
|
+
~batchSizeTarget,
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Enter the reorg threshold: shrink each chain's buffer by its configured
|
|
129
|
+
// blockLag and flip the flag.
|
|
130
|
+
let enterReorgThreshold = (crossChainState: t) => {
|
|
131
|
+
Logging.info("Reorg threshold reached")
|
|
132
|
+
Prometheus.ReorgThreshold.set(~isInReorgThreshold=true)
|
|
133
|
+
|
|
134
|
+
for i in 0 to crossChainState.chainIds->Array.length - 1 {
|
|
135
|
+
crossChainState
|
|
136
|
+
->getChainState(crossChainState.chainIds->Array.getUnsafe(i))
|
|
137
|
+
->ChainState.enterReorgThreshold
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
crossChainState.isInReorgThreshold = true
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Commit each progressed chain's batch progress, then decide readiness for the
|
|
144
|
+
// whole indexer. A chain is marked caught up only once EVERY chain is caught up
|
|
145
|
+
// (reached endblock or fetched/processed to head) with no processable events
|
|
146
|
+
// left — so no chain flips to ready while another is still backfilling.
|
|
147
|
+
let applyBatchProgress = (crossChainState: t, ~batch: Batch.t) => {
|
|
148
|
+
let chainIds = crossChainState.chainIds
|
|
149
|
+
|
|
150
|
+
let everyChainCaughtUp = ref(true)
|
|
151
|
+
for i in 0 to chainIds->Array.length - 1 {
|
|
152
|
+
let cs = crossChainState->getChainState(chainIds->Array.getUnsafe(i))
|
|
153
|
+
cs->ChainState.applyBatchProgress(~batch)
|
|
154
|
+
if !(cs->ChainState.hasProcessedToEndblock || cs->ChainState.isProgressAtHead) {
|
|
155
|
+
everyChainCaughtUp := false
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
let indexerCaughtUp = crossChainState->nextItemIsNone && everyChainCaughtUp.contents
|
|
160
|
+
|
|
161
|
+
let allChainsReady = ref(true)
|
|
162
|
+
for i in 0 to chainIds->Array.length - 1 {
|
|
163
|
+
let cs = crossChainState->getChainState(chainIds->Array.getUnsafe(i))
|
|
164
|
+
if indexerCaughtUp {
|
|
165
|
+
cs->ChainState.markReady
|
|
166
|
+
}
|
|
167
|
+
if !(cs->ChainState.isReady) {
|
|
168
|
+
allChainsReady := false
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if allChainsReady.contents {
|
|
173
|
+
Prometheus.ProgressReady.setAllReady()
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
crossChainState.isRealtime = crossChainState.isRealtime || allChainsReady.contents
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// --- Fetch control. ---
|
|
180
|
+
|
|
181
|
+
// Chains ordered furthest-behind first, so the shared buffer pool goes to the
|
|
182
|
+
// chains with the most backfill work before the rest.
|
|
183
|
+
let priorityOrder = (crossChainState: t) =>
|
|
184
|
+
crossChainState.chainStates
|
|
185
|
+
->Dict.valuesToArray
|
|
186
|
+
->Array.toSorted((a, b) =>
|
|
187
|
+
Float.compare(
|
|
188
|
+
a->ChainState.fetchState->FetchState.getProgressPercentage,
|
|
189
|
+
b->ChainState.fetchState->FetchState.getProgressPercentage,
|
|
190
|
+
)
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
// In-flight estimated items across every chain — the live draw against
|
|
194
|
+
// targetBufferSize alongside totalReadyCount, so the pool isn't re-dispatched
|
|
195
|
+
// while queries are still being fetched.
|
|
196
|
+
let totalReservedSize = (crossChainState: t) => {
|
|
197
|
+
let total = ref(0.)
|
|
198
|
+
for i in 0 to crossChainState.chainIds->Array.length - 1 {
|
|
199
|
+
let cs = crossChainState->getChainState(crossChainState.chainIds->Array.getUnsafe(i))
|
|
200
|
+
total := total.contents +. cs->ChainState.pendingBudget
|
|
201
|
+
}
|
|
202
|
+
total.contents
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Furthest-behind first: order candidate queries by the chain progress % at
|
|
206
|
+
// their fromBlock, so the most behind ranges across all chains are admitted
|
|
207
|
+
// before the rest.
|
|
208
|
+
let compareByProgress = (a: FetchState.query, b: FetchState.query) =>
|
|
209
|
+
Float.compare(a.progress, b.progress)
|
|
210
|
+
|
|
211
|
+
// Dispatch a fetch tick across the whole indexer from one shared pool of
|
|
212
|
+
// ~targetBufferSize ready events. Every chain proposes its candidate queries
|
|
213
|
+
// (each carrying an estimated response size) against the full free budget; the
|
|
214
|
+
// candidates are then pooled, ordered by chain progress (furthest-behind first),
|
|
215
|
+
// and admitted until the budget is consumed. So the budget is split per query
|
|
216
|
+
// across chains rather than per chain — a chain that can only use a little
|
|
217
|
+
// leaves the rest for the others automatically.
|
|
218
|
+
let checkAndFetch = async (
|
|
219
|
+
crossChainState: t,
|
|
220
|
+
~dispatchChain: (~chain: ChainMap.Chain.t, ~action: FetchState.nextQuery) => promise<unit>,
|
|
221
|
+
) => {
|
|
222
|
+
let remaining = Pervasives.max(
|
|
223
|
+
0,
|
|
224
|
+
crossChainState.targetBufferSize -
|
|
225
|
+
crossChainState->totalReadyCount -
|
|
226
|
+
crossChainState->totalReservedSize->Float.toInt,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
let chainIds = crossChainState.chainIds
|
|
230
|
+
let actionByChain = Dict.make()
|
|
231
|
+
// Candidate queries from every chain. Each query carries its chain id and the
|
|
232
|
+
// chain progress % at its fromBlock (the admission sort key), set here so the
|
|
233
|
+
// pool can be ordered without a side tuple per query.
|
|
234
|
+
let candidates = []
|
|
235
|
+
for i in 0 to chainIds->Array.length - 1 {
|
|
236
|
+
let chainId = chainIds->Array.getUnsafe(i)
|
|
237
|
+
let cs = crossChainState->getChainState(chainId)
|
|
238
|
+
let fetchState = cs->ChainState.fetchState
|
|
239
|
+
switch fetchState->FetchState.getNextQuery(
|
|
240
|
+
~budget=remaining,
|
|
241
|
+
~chainPendingBudget=cs->ChainState.pendingBudget,
|
|
242
|
+
) {
|
|
243
|
+
| (WaitingForNewBlock | NothingToQuery) as action =>
|
|
244
|
+
actionByChain->Utils.Dict.setByInt(chainId, action)
|
|
245
|
+
| Ready(queries) =>
|
|
246
|
+
// Default to NothingToQuery; replaced below if any candidate is admitted.
|
|
247
|
+
actionByChain->Utils.Dict.setByInt(chainId, FetchState.NothingToQuery)
|
|
248
|
+
queries->Array.forEach(query => {
|
|
249
|
+
query.chainId = chainId
|
|
250
|
+
query.progress =
|
|
251
|
+
fetchState->FetchState.getProgressPercentageAt(~blockNumber=query.fromBlock)
|
|
252
|
+
candidates->Array.push(query)
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
candidates->Array.sort(compareByProgress)
|
|
258
|
+
|
|
259
|
+
// Admit furthest-behind first until the budget runs out. The condition is
|
|
260
|
+
// checked before each query, so as long as there's any budget left we admit
|
|
261
|
+
// the next one even when its estimate alone exceeds the remainder — otherwise a
|
|
262
|
+
// chain whose only query is bigger than the budget would never make progress.
|
|
263
|
+
let admittedByChain = Dict.make()
|
|
264
|
+
let running = ref(0.)
|
|
265
|
+
let remainingF = remaining->Int.toFloat
|
|
266
|
+
let idx = ref(0)
|
|
267
|
+
while running.contents < remainingF && idx.contents < candidates->Array.length {
|
|
268
|
+
let query = candidates->Array.getUnsafe(idx.contents)
|
|
269
|
+
admittedByChain->Utils.Dict.push(query.chainId->Int.toString, query)
|
|
270
|
+
running := running.contents +. query.estResponseSize
|
|
271
|
+
idx := idx.contents + 1
|
|
272
|
+
}
|
|
273
|
+
admittedByChain->Dict.forEachWithKey((queries, chainId) => {
|
|
274
|
+
actionByChain->Dict.set(chainId, FetchState.Ready(queries))
|
|
275
|
+
// Mark the admitted queries in flight and reserve their size against the
|
|
276
|
+
// shared budget; released as each response lands in handleQueryResult.
|
|
277
|
+
crossChainState
|
|
278
|
+
->getChainState(chainId->Int.fromString->Option.getUnsafe)
|
|
279
|
+
->ChainState.startFetchingQueries(~queries)
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
let promises = []
|
|
283
|
+
for i in 0 to chainIds->Array.length - 1 {
|
|
284
|
+
let chainId = chainIds->Array.getUnsafe(i)
|
|
285
|
+
switch actionByChain->Utils.Dict.dangerouslyGetByIntNonOption(chainId) {
|
|
286
|
+
| Some(NothingToQuery)
|
|
287
|
+
| None => ()
|
|
288
|
+
| Some(action) =>
|
|
289
|
+
let chain = ChainMap.Chain.makeUnsafe(~chainId)
|
|
290
|
+
promises->Array.push(dispatchChain(~chain, ~action))
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
let _ = await promises->Promise.all
|
|
294
|
+
}
|