envio 3.3.0-alpha.8 → 3.3.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/index.d.ts +19 -0
- package/licenses/CLA.md +35 -0
- package/licenses/EULA.md +67 -0
- package/licenses/LICENSE.md +45 -0
- package/licenses/README.md +35 -0
- package/package.json +9 -8
- package/src/Batch.res.mjs +1 -1
- package/src/BatchProcessing.res +0 -7
- package/src/BatchProcessing.res.mjs +1 -8
- package/src/ChainFetching.res +30 -18
- package/src/ChainFetching.res.mjs +23 -13
- package/src/ChainState.res +205 -59
- package/src/ChainState.res.mjs +130 -27
- package/src/ChainState.resi +16 -1
- package/src/Config.res +9 -5
- package/src/Config.res.mjs +2 -2
- package/src/Core.res +20 -0
- package/src/Core.res.mjs +12 -0
- package/src/CrossChainState.res +147 -39
- package/src/CrossChainState.res.mjs +42 -10
- package/src/EffectState.res +100 -0
- package/src/EffectState.res.mjs +74 -0
- package/src/EffectState.resi +32 -0
- package/src/Envio.res +25 -7
- package/src/Envio.res.mjs +15 -19
- package/src/EnvioGlobal.res +53 -0
- package/src/EnvioGlobal.res.mjs +31 -0
- package/src/EventConfigBuilder.res +30 -56
- package/src/EventConfigBuilder.res.mjs +24 -30
- package/src/EventProcessing.res +8 -5
- package/src/EventProcessing.res.mjs +2 -1
- package/src/FetchState.res +842 -474
- package/src/FetchState.res.mjs +545 -356
- package/src/HandlerLoader.res +1 -1
- package/src/HandlerLoader.res.mjs +1 -1
- package/src/HandlerRegister.res +478 -305
- package/src/HandlerRegister.res.mjs +277 -209
- package/src/HandlerRegister.resi +11 -6
- package/src/InMemoryStore.res +14 -26
- package/src/InMemoryStore.res.mjs +6 -19
- package/src/IndexerState.res +15 -39
- package/src/IndexerState.res.mjs +18 -29
- package/src/IndexerState.resi +2 -10
- package/src/Internal.res +121 -18
- package/src/Internal.res.mjs +96 -2
- package/src/LoadLayer.res +44 -24
- package/src/LoadLayer.res.mjs +43 -20
- package/src/LoadLayer.resi +1 -0
- package/src/LogSelection.res +92 -217
- package/src/LogSelection.res.mjs +95 -184
- package/src/Main.res +29 -143
- package/src/Main.res.mjs +25 -88
- package/src/Metrics.res +1 -1
- package/src/Metrics.res.mjs +1 -1
- package/src/Persistence.res +12 -3
- package/src/PgStorage.res +155 -82
- package/src/PgStorage.res.mjs +130 -77
- package/src/Prometheus.res +20 -10
- package/src/Prometheus.res.mjs +22 -10
- package/src/PruneStaleHistory.res +146 -35
- package/src/PruneStaleHistory.res.mjs +114 -20
- package/src/RawEvent.res +2 -2
- package/src/Rollback.res +32 -13
- package/src/Rollback.res.mjs +24 -11
- package/src/RollbackCommit.res +4 -1
- package/src/RollbackCommit.res.mjs +3 -2
- package/src/SimulateDeadInputTracker.res +1 -1
- package/src/SimulateItems.res +39 -7
- package/src/SimulateItems.res.mjs +28 -9
- package/src/TestIndexer.res +2 -2
- package/src/TestIndexer.res.mjs +2 -2
- package/src/TopicFilter.res +1 -25
- package/src/TopicFilter.res.mjs +0 -74
- package/src/UserContext.res +62 -23
- package/src/UserContext.res.mjs +26 -6
- package/src/Writing.res +60 -21
- package/src/Writing.res.mjs +27 -9
- package/src/bindings/NodeJs.res +5 -0
- package/src/bindings/Viem.res +0 -5
- package/src/sources/EventRouter.res +0 -21
- package/src/sources/EventRouter.res.mjs +0 -12
- package/src/sources/Evm.res +4 -1
- package/src/sources/Evm.res.mjs +1 -1
- package/src/sources/EvmChain.res +2 -27
- package/src/sources/EvmChain.res.mjs +2 -23
- package/src/sources/EvmRpcClient.res +58 -23
- package/src/sources/EvmRpcClient.res.mjs +11 -5
- package/src/sources/Fuel.res +3 -1
- package/src/sources/Fuel.res.mjs +1 -1
- package/src/sources/HyperSync.res +9 -38
- package/src/sources/HyperSync.res.mjs +16 -28
- package/src/sources/HyperSync.resi +2 -2
- package/src/sources/HyperSyncClient.res +88 -11
- package/src/sources/HyperSyncClient.res.mjs +39 -6
- package/src/sources/HyperSyncSource.res +18 -210
- package/src/sources/HyperSyncSource.res.mjs +9 -137
- package/src/sources/Rpc.res +0 -32
- package/src/sources/Rpc.res.mjs +1 -46
- package/src/sources/RpcSource.res +129 -533
- package/src/sources/RpcSource.res.mjs +161 -379
- package/src/sources/SimulateSource.res +37 -19
- package/src/sources/SimulateSource.res.mjs +27 -10
- package/src/sources/SourceManager.res +4 -14
- package/src/sources/SourceManager.res.mjs +3 -9
- package/src/sources/SourceManager.resi +0 -2
- package/src/sources/SvmHyperSyncSource.res +13 -3
- package/src/sources/SvmHyperSyncSource.res.mjs +1 -1
package/src/ChainState.resi
CHANGED
|
@@ -8,6 +8,7 @@ let configAddresses: Config.chain => array<Internal.indexingAddress>
|
|
|
8
8
|
let make: (
|
|
9
9
|
~chainConfig: Config.chain,
|
|
10
10
|
~fetchState: FetchState.t,
|
|
11
|
+
~onEventRegistrations: array<Internal.onEventRegistration>=?,
|
|
11
12
|
~indexingAddresses: IndexingAddresses.t,
|
|
12
13
|
~sourceManager: SourceManager.t,
|
|
13
14
|
~reorgDetection: ReorgDetection.t,
|
|
@@ -60,11 +61,21 @@ let bufferSize: t => int
|
|
|
60
61
|
let bufferReadyCount: t => int
|
|
61
62
|
let getProgressPercentage: t => float
|
|
62
63
|
let chainDensity: t => option<float>
|
|
64
|
+
let effectiveDensity: t => option<float>
|
|
63
65
|
let hasReadyItem: t => bool
|
|
64
66
|
let isReadyToEnterReorgThreshold: t => bool
|
|
65
67
|
|
|
66
68
|
// Fetch control.
|
|
67
|
-
let
|
|
69
|
+
let targetBlock: (t, ~chainTargetItems: float) => int
|
|
70
|
+
let blockAtProgress: (t, ~progress: float) => int
|
|
71
|
+
let frontierProgress: t => float
|
|
72
|
+
let getNextQuery: (
|
|
73
|
+
t,
|
|
74
|
+
~chainTargetItems: float,
|
|
75
|
+
~chunkItemsMultiplier: float=?,
|
|
76
|
+
~itemsTargetFloor: int=?,
|
|
77
|
+
~maxTargetBlock: int=?,
|
|
78
|
+
) => FetchState.nextQuery
|
|
68
79
|
let dispatch: (
|
|
69
80
|
t,
|
|
70
81
|
~executeQuery: FetchState.query => promise<unit>,
|
|
@@ -103,6 +114,10 @@ let isReady: t => bool
|
|
|
103
114
|
let isFetchingAtHead: t => bool
|
|
104
115
|
let isAtHeadWithoutEndBlock: t => bool
|
|
105
116
|
|
|
117
|
+
// Drop over-fetched events an address-param filter rejects, before contract
|
|
118
|
+
// registration runs on them. Uses the chain's current indexing addresses.
|
|
119
|
+
let filterByClientAddress: (t, array<Internal.item>) => array<Internal.item>
|
|
120
|
+
|
|
106
121
|
// State transitions. The chain state is mutated only through these.
|
|
107
122
|
let handleQueryResult: (
|
|
108
123
|
t,
|
package/src/Config.res
CHANGED
|
@@ -200,14 +200,18 @@ let svmEventDescriptorSchema = S.schema(s =>
|
|
|
200
200
|
"transactionFields": s.matches(S.array(Internal.svmTransactionFieldSchema)),
|
|
201
201
|
"blockFields": s.matches(S.option(S.array(Internal.svmBlockFieldSchema))),
|
|
202
202
|
"includeLogs": s.matches(S.bool),
|
|
203
|
+
// An array of AND-groups OR-ed together: the CLI normalizes both the flat
|
|
204
|
+
// and `any_of` YAML shapes to `Vec<Vec<SvmAccountFilterJson>>`.
|
|
203
205
|
"accountFilters": s.matches(
|
|
204
206
|
S.option(
|
|
205
207
|
S.array(
|
|
206
|
-
S.
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
208
|
+
S.array(
|
|
209
|
+
S.schema(s =>
|
|
210
|
+
{
|
|
211
|
+
"position": s.matches(S.int),
|
|
212
|
+
"values": s.matches(S.array(S.string)),
|
|
213
|
+
}
|
|
214
|
+
),
|
|
211
215
|
),
|
|
212
216
|
),
|
|
213
217
|
),
|
package/src/Config.res.mjs
CHANGED
|
@@ -115,10 +115,10 @@ let svmEventDescriptorSchema = S$RescriptSchema.schema(s => ({
|
|
|
115
115
|
transactionFields: s.m(S$RescriptSchema.array(Internal.svmTransactionFieldSchema)),
|
|
116
116
|
blockFields: s.m(S$RescriptSchema.option(S$RescriptSchema.array(Internal.svmBlockFieldSchema))),
|
|
117
117
|
includeLogs: s.m(S$RescriptSchema.bool),
|
|
118
|
-
accountFilters: s.m(S$RescriptSchema.option(S$RescriptSchema.array(S$RescriptSchema.schema(s => ({
|
|
118
|
+
accountFilters: s.m(S$RescriptSchema.option(S$RescriptSchema.array(S$RescriptSchema.array(S$RescriptSchema.schema(s => ({
|
|
119
119
|
position: s.m(S$RescriptSchema.int),
|
|
120
120
|
values: s.m(S$RescriptSchema.array(S$RescriptSchema.string))
|
|
121
|
-
}))))),
|
|
121
|
+
})))))),
|
|
122
122
|
isInner: s.m(S$RescriptSchema.option(S$RescriptSchema.bool)),
|
|
123
123
|
accounts: s.m(S$RescriptSchema.option(S$RescriptSchema.array(S$RescriptSchema.string))),
|
|
124
124
|
args: s.m(S$RescriptSchema.option(S$RescriptSchema.json(false)))
|
package/src/Core.res
CHANGED
|
@@ -10,9 +10,17 @@ type svmHypersyncClientCtor
|
|
|
10
10
|
type hyperfuelClientCtor
|
|
11
11
|
type transactionStoreCtor
|
|
12
12
|
type blockStoreCtor
|
|
13
|
+
type parseConfigYamlOptions = {
|
|
14
|
+
schema?: string,
|
|
15
|
+
env?: dict<string>,
|
|
16
|
+
files?: dict<string>,
|
|
17
|
+
isRescript?: bool,
|
|
18
|
+
}
|
|
13
19
|
|
|
14
20
|
type addon = {
|
|
15
21
|
getConfigJson: (~configPath: Null.t<string>, ~directory: Null.t<string>) => string,
|
|
22
|
+
encodeIndexedTopic: (~abiType: string, ~value: unknown) => EvmTypes.Hex.t,
|
|
23
|
+
parseConfigYaml: (string, parseConfigYamlOptions) => string,
|
|
16
24
|
runCli: (~args: array<string>, ~envioPackageDir: Null.t<string>) => promise<Null.t<string>>,
|
|
17
25
|
@as("EvmHypersyncClient")
|
|
18
26
|
evmHypersyncClient: evmHypersyncClientCtor,
|
|
@@ -197,6 +205,18 @@ let getConfigJson = (~configPath=?, ~directory=?) => {
|
|
|
197
205
|
)
|
|
198
206
|
}
|
|
199
207
|
|
|
208
|
+
// Pure config entry point: no cwd, config file, schema file, .env, or process env lookup.
|
|
209
|
+
// A supplied schema is authoritative; omitted or blank schema text means an empty schema.
|
|
210
|
+
let parseConfigYaml = (~schema=?, ~env=?, ~files=?, ~isRescript=false, yaml) => {
|
|
211
|
+
let addon = getAddon()
|
|
212
|
+
addon.parseConfigYaml(yaml, {
|
|
213
|
+
?schema,
|
|
214
|
+
?env,
|
|
215
|
+
?files,
|
|
216
|
+
isRescript,
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
|
|
200
220
|
let runCli = args => {
|
|
201
221
|
let addon = getAddon()
|
|
202
222
|
addon.runCli(~args, ~envioPackageDir=Null.make(envioPackageDir))
|
package/src/Core.res.mjs
CHANGED
|
@@ -180,6 +180,17 @@ function getConfigJson(configPath, directory) {
|
|
|
180
180
|
return addon.getConfigJson(Stdlib_Null.fromOption(configPath), Stdlib_Null.fromOption(directory));
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
function parseConfigYaml(schema, env, files, isRescriptOpt, yaml) {
|
|
184
|
+
let isRescript = isRescriptOpt !== undefined ? isRescriptOpt : false;
|
|
185
|
+
let addon = getAddon();
|
|
186
|
+
return addon.parseConfigYaml(yaml, {
|
|
187
|
+
schema: schema,
|
|
188
|
+
env: env,
|
|
189
|
+
files: files,
|
|
190
|
+
isRescript: isRescript
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
183
194
|
function runCli(args) {
|
|
184
195
|
let addon = getAddon();
|
|
185
196
|
return addon.runCli(args, envioPackageDir);
|
|
@@ -196,6 +207,7 @@ export {
|
|
|
196
207
|
addonRef,
|
|
197
208
|
getAddon,
|
|
198
209
|
getConfigJson,
|
|
210
|
+
parseConfigYaml,
|
|
199
211
|
runCli,
|
|
200
212
|
}
|
|
201
213
|
/* envioPackageDir Not a pure module */
|
package/src/CrossChainState.res
CHANGED
|
@@ -19,7 +19,7 @@ type t = {
|
|
|
19
19
|
let calculateTargetBufferSize = () =>
|
|
20
20
|
switch Env.targetBufferSize {
|
|
21
21
|
| Some(size) => size
|
|
22
|
-
| None =>
|
|
22
|
+
| None => 100_000
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
let make = (
|
|
@@ -169,13 +169,18 @@ let applyBatchProgress = (crossChainState: t, ~batch: Batch.t, ~blockTimestampNa
|
|
|
169
169
|
|
|
170
170
|
// --- Fetch control. ---
|
|
171
171
|
|
|
172
|
-
// Chains ordered furthest-behind first
|
|
173
|
-
// chains with the most backfill work
|
|
172
|
+
// Chains ordered furthest-behind first by fetch-frontier progress, so the
|
|
173
|
+
// shared buffer pool goes to the chains with the most fetchable backfill work
|
|
174
|
+
// before the rest — and the same metric that sets the alignment line also
|
|
175
|
+
// decides who draws budget first, so the anchor is served before any chain it
|
|
176
|
+
// clamps. (Batch ordering keeps its own getProgressPercentage measure.) A chain
|
|
177
|
+
// with no known height reads 100% here and sorts last, which is fine: it can't
|
|
178
|
+
// fetch until its first block lands regardless of when it's visited.
|
|
174
179
|
let priorityOrder = (crossChainState: t) =>
|
|
175
180
|
crossChainState.chainStates
|
|
176
181
|
->Dict.valuesToArray
|
|
177
182
|
->Array.toSorted((a, b) =>
|
|
178
|
-
Float.compare(a->ChainState.
|
|
183
|
+
Float.compare(a->ChainState.frontierProgress, b->ChainState.frontierProgress)
|
|
179
184
|
)
|
|
180
185
|
|
|
181
186
|
// In-flight estimated items across every chain — the live draw against
|
|
@@ -190,6 +195,22 @@ let totalReservedSize = (crossChainState: t) => {
|
|
|
190
195
|
total.contents
|
|
191
196
|
}
|
|
192
197
|
|
|
198
|
+
// Action for a chain that was handed budget but emitted no query (its budget
|
|
199
|
+
// went to more-behind chains, or the alignment clamp cut its range to
|
|
200
|
+
// nothing). A chain is genuinely idle — and correctly left undispatched —
|
|
201
|
+
// when it is caught up to its head/endblock, still draining in-flight
|
|
202
|
+
// queries, or holding ready items that batch processing will drain and
|
|
203
|
+
// re-schedule from. Any other chain must keep polling for new blocks instead
|
|
204
|
+
// of going silent: NothingToQuery isn't dispatched, and with the pool
|
|
205
|
+
// unsaturated nothing else guarantees a tick that would revisit it, so its
|
|
206
|
+
// head tracking would freeze.
|
|
207
|
+
let idleOrWaitAction = (cs: ChainState.t) =>
|
|
208
|
+
cs->ChainState.isFetchingAtHead ||
|
|
209
|
+
cs->ChainState.pendingBudget > 0. ||
|
|
210
|
+
cs->ChainState.bufferReadyCount > 0
|
|
211
|
+
? FetchState.NothingToQuery
|
|
212
|
+
: FetchState.WaitingForNewBlock
|
|
213
|
+
|
|
193
214
|
// Dispatch a fetch tick across the whole indexer from one shared pool of
|
|
194
215
|
// ~targetBufferSize ready events, as a waterfall: visit chains furthest-behind
|
|
195
216
|
// first, hand each the budget remaining at that point (plus its own
|
|
@@ -198,56 +219,143 @@ let totalReservedSize = (crossChainState: t) => {
|
|
|
198
219
|
// chain-density-derived target block, then subtract what it actually used
|
|
199
220
|
// before moving to the next chain. So a chain that can only use a little
|
|
200
221
|
// (density too low, or already caught up) leaves the rest for the others
|
|
201
|
-
// automatically
|
|
202
|
-
//
|
|
222
|
+
// automatically. Starting a new query requires at least 10% of the target pool
|
|
223
|
+
// to be free. A chain visited after the budget falls below that admission unit
|
|
224
|
+
// doesn't query this round — reservations release as responses land, so the
|
|
225
|
+
// next tick redistributes. Every other chain is additionally capped at the
|
|
226
|
+
// lowest-frontier-progress chain's progress mapped onto its own range, so no
|
|
227
|
+
// chain runs ahead of the chain the pool is prioritizing — including on ticks
|
|
228
|
+
// where that chain is mid-fetch and emits no new query. A chain with no known
|
|
229
|
+
// height can't anchor this line (there's no range to measure against), a chain
|
|
230
|
+
// caught up to its fetchable head reads 100% and so never anchors while another
|
|
231
|
+
// is behind, and once the whole indexer has caught up (isRealtime) the clamp is
|
|
232
|
+
// dropped — chains at head only trail each other by real-time block production.
|
|
203
233
|
let checkAndFetch = async (
|
|
204
234
|
crossChainState: t,
|
|
205
235
|
~dispatchChain: (~chain: ChainMap.Chain.t, ~action: FetchState.nextQuery) => promise<unit>,
|
|
206
236
|
) => {
|
|
237
|
+
let targetBudget = crossChainState.targetBufferSize->Int.toFloat
|
|
207
238
|
let remaining = ref(
|
|
208
239
|
Pervasives.max(
|
|
209
240
|
0.,
|
|
210
|
-
|
|
241
|
+
targetBudget -.
|
|
211
242
|
crossChainState->totalReadyCount->Int.toFloat -.
|
|
212
243
|
crossChainState->totalReservedSize,
|
|
213
244
|
),
|
|
214
245
|
)
|
|
215
246
|
|
|
247
|
+
// New fetch work is admitted in units of 10% of the target pool. Waiting
|
|
248
|
+
// below this floor avoids spending the last few free items on undersized
|
|
249
|
+
// queries; response and batch completion schedule another tick after they
|
|
250
|
+
// release enough budget.
|
|
251
|
+
let minimumAdmissionBudget = targetBudget *. 0.1
|
|
252
|
+
|
|
253
|
+
// A chain with no density signal probes blind, so it only gets a bounded
|
|
254
|
+
// slice of the pool — one unknown chain shouldn't hold the whole budget
|
|
255
|
+
// while it takes its first measurements. Its probe is one admission unit.
|
|
256
|
+
let coldChainBudget = minimumAdmissionBudget
|
|
257
|
+
|
|
258
|
+
// Chunk reservations get headroom over the density estimate so a
|
|
259
|
+
// denser-than-expected range doesn't truncate at the server cap; realtime
|
|
260
|
+
// gets more since a forced catch-up query there costs a head-poll roundtrip.
|
|
261
|
+
let chunkItemsMultiplier = crossChainState.isRealtime ? 3. : 1.5
|
|
262
|
+
|
|
263
|
+
// Server-cap floor for bounded queries: their block range is already the
|
|
264
|
+
// hard bound on the response, so a low density estimate shrinking the cap
|
|
265
|
+
// below this only buys self-truncated responses. Splitting the target pool
|
|
266
|
+
// across a chain's concurrency slots keeps the worst case — every in-flight
|
|
267
|
+
// bounded query returning a full floored response at once — at ~one buffer
|
|
268
|
+
// target.
|
|
269
|
+
let itemsTargetFloor = crossChainState.targetBufferSize / FetchState.maxChainConcurrency
|
|
270
|
+
|
|
271
|
+
let prioritizedChainStates = crossChainState->priorityOrder
|
|
272
|
+
|
|
273
|
+
// Alignment anchor: the first known-height chain in priority order — which,
|
|
274
|
+
// since that order sorts by frontier progress, is the chain furthest behind
|
|
275
|
+
// by the very metric the clamp maps other chains against. Anchoring on the
|
|
276
|
+
// frontier (not on the target of whichever chain happens to query this tick)
|
|
277
|
+
// keeps the line in place while the anchor's queries are still in flight. A
|
|
278
|
+
// chain caught up to its fetchable head reads 100% and sorts past any behind
|
|
279
|
+
// chain, so it never anchors while another chain is behind.
|
|
280
|
+
let alignment = crossChainState.isRealtime
|
|
281
|
+
? None
|
|
282
|
+
: prioritizedChainStates
|
|
283
|
+
->Array.find(cs => cs->ChainState.knownHeight != 0)
|
|
284
|
+
->Option.map(cs => ((cs->ChainState.chainConfig).id, cs->ChainState.frontierProgress))
|
|
285
|
+
|
|
216
286
|
let actionByChain = Dict.make()
|
|
217
|
-
|
|
218
|
-
->priorityOrder
|
|
219
|
-
->Array.forEach(cs => {
|
|
287
|
+
prioritizedChainStates->Array.forEach(cs => {
|
|
220
288
|
let chainId = (cs->ChainState.chainConfig).id
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
289
|
+
if cs->ChainState.knownHeight == 0 {
|
|
290
|
+
// No height yet — there's nothing to size a query against, only height
|
|
291
|
+
// tracking to start. Checked before the admission floor so a chain that
|
|
292
|
+
// hasn't found its first block yet keeps polling even while other chains
|
|
293
|
+
// hold the whole pool. (The general can't-fetch-yet rule, including
|
|
294
|
+
// blockLag, lives in FetchState.getNextQuery — this branch only
|
|
295
|
+
// short-circuits the unambiguous no-height case.)
|
|
296
|
+
actionByChain->Utils.Dict.setByInt(chainId, FetchState.WaitingForNewBlock)
|
|
297
|
+
} else if remaining.contents < minimumAdmissionBudget {
|
|
298
|
+
// More than 90% of the target pool is ready or reserved. Don't admit new
|
|
299
|
+
// queries until a full admission unit becomes free. No wake-up poll is
|
|
300
|
+
// needed: a saturated pool means some chain holds ready items or
|
|
301
|
+
// in-flight reservations, so a batch completion or landing response is
|
|
302
|
+
// guaranteed to schedule another tick that revisits this chain.
|
|
303
|
+
actionByChain->Utils.Dict.setByInt(chainId, FetchState.NothingToQuery)
|
|
304
|
+
} else {
|
|
305
|
+
let isCold = cs->ChainState.effectiveDensity === None
|
|
306
|
+
let chainTargetItems =
|
|
307
|
+
(isCold ? Pervasives.min(remaining.contents, coldChainBudget) : remaining.contents) +.
|
|
308
|
+
cs->ChainState.pendingBudget
|
|
309
|
+
let maxTargetBlock = switch alignment {
|
|
310
|
+
// 5% margin past the anchor's line: chains whose progress tracks the
|
|
311
|
+
// anchor closely would otherwise flap in and out of the clamp on every
|
|
312
|
+
// small frontier move, stalling their pipeline every other tick.
|
|
313
|
+
| Some((anchorChainId, progress)) if anchorChainId !== chainId =>
|
|
314
|
+
Some(cs->ChainState.blockAtProgress(~progress=progress +. 0.05))
|
|
315
|
+
| _ => None
|
|
316
|
+
}
|
|
317
|
+
switch cs->ChainState.getNextQuery(
|
|
318
|
+
~chainTargetItems,
|
|
319
|
+
~chunkItemsMultiplier,
|
|
320
|
+
~itemsTargetFloor,
|
|
321
|
+
~maxTargetBlock?,
|
|
322
|
+
) {
|
|
323
|
+
| WaitingForNewBlock as action => actionByChain->Utils.Dict.setByInt(chainId, action)
|
|
324
|
+
| NothingToQuery =>
|
|
325
|
+
// A chain below its head can emit no query when its budget went to
|
|
326
|
+
// more-behind chains or the cross-chain alignment clamped its range to
|
|
327
|
+
// nothing — idleOrWaitAction keeps it polling for new blocks.
|
|
328
|
+
actionByChain->Utils.Dict.setByInt(chainId, idleOrWaitAction(cs))
|
|
329
|
+
| Ready(queries) => {
|
|
330
|
+
let consumed =
|
|
331
|
+
queries->Array.reduce(0., (acc, query: FetchState.query) =>
|
|
332
|
+
acc +. query.itemsEst->Int.toFloat
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
let partitions = Dict.make()
|
|
336
|
+
queries->Array.forEach((query: FetchState.query) =>
|
|
337
|
+
partitions->Dict.set(
|
|
338
|
+
query.partitionId,
|
|
339
|
+
{
|
|
340
|
+
"fromBlock": query.fromBlock,
|
|
341
|
+
"targetBlock": query.toBlock,
|
|
342
|
+
"targetEvents": query.itemsTarget,
|
|
343
|
+
},
|
|
344
|
+
)
|
|
238
345
|
)
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
346
|
+
Logging.trace({
|
|
347
|
+
"msg": "Started querying",
|
|
348
|
+
"chainId": chainId,
|
|
349
|
+
"partitions": partitions,
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
actionByChain->Utils.Dict.setByInt(chainId, FetchState.Ready(queries))
|
|
353
|
+
// Mark the queries in flight and reserve their size against the
|
|
354
|
+
// shared budget; released as each response lands in
|
|
355
|
+
// handleQueryResult.
|
|
356
|
+
cs->ChainState.startFetchingQueries(~queries)
|
|
357
|
+
remaining := Pervasives.max(0., remaining.contents -. consumed)
|
|
358
|
+
}
|
|
251
359
|
}
|
|
252
360
|
}
|
|
253
361
|
})
|
|
@@ -6,8 +6,11 @@ import * as Utils from "./Utils.res.mjs";
|
|
|
6
6
|
import * as Logging from "./Logging.res.mjs";
|
|
7
7
|
import * as ChainMap from "./ChainMap.res.mjs";
|
|
8
8
|
import * as ChainState from "./ChainState.res.mjs";
|
|
9
|
+
import * as FetchState from "./FetchState.res.mjs";
|
|
9
10
|
import * as Prometheus from "./Prometheus.res.mjs";
|
|
10
11
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
12
|
+
import * as Primitive_int from "@rescript/runtime/lib/es6/Primitive_int.js";
|
|
13
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
11
14
|
import * as Primitive_float from "@rescript/runtime/lib/es6/Primitive_float.js";
|
|
12
15
|
import * as SafeCheckpointTracking from "./SafeCheckpointTracking.res.mjs";
|
|
13
16
|
|
|
@@ -15,13 +18,13 @@ function calculateTargetBufferSize() {
|
|
|
15
18
|
if (Env.targetBufferSize !== undefined) {
|
|
16
19
|
return Env.targetBufferSize;
|
|
17
20
|
} else {
|
|
18
|
-
return
|
|
21
|
+
return 100000;
|
|
19
22
|
}
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
function make(chainStates, isInReorgThreshold, isRealtime, targetBufferSizeOpt) {
|
|
23
26
|
let targetBufferSize = targetBufferSizeOpt !== undefined ? targetBufferSizeOpt : (
|
|
24
|
-
Env.targetBufferSize !== undefined ? Env.targetBufferSize :
|
|
27
|
+
Env.targetBufferSize !== undefined ? Env.targetBufferSize : 100000
|
|
25
28
|
);
|
|
26
29
|
let crossChainState = {
|
|
27
30
|
chainStates: chainStates,
|
|
@@ -121,7 +124,7 @@ function applyBatchProgress(crossChainState, batch, blockTimestampName) {
|
|
|
121
124
|
}
|
|
122
125
|
|
|
123
126
|
function priorityOrder(crossChainState) {
|
|
124
|
-
return Object.values(crossChainState.chainStates).toSorted((a, b) => Primitive_float.compare(ChainState.
|
|
127
|
+
return Object.values(crossChainState.chainStates).toSorted((a, b) => Primitive_float.compare(ChainState.frontierProgress(a), ChainState.frontierProgress(b)));
|
|
125
128
|
}
|
|
126
129
|
|
|
127
130
|
function totalReservedSize(crossChainState) {
|
|
@@ -133,31 +136,60 @@ function totalReservedSize(crossChainState) {
|
|
|
133
136
|
return total;
|
|
134
137
|
}
|
|
135
138
|
|
|
139
|
+
function idleOrWaitAction(cs) {
|
|
140
|
+
if (ChainState.isFetchingAtHead(cs) || ChainState.pendingBudget(cs) > 0 || ChainState.bufferReadyCount(cs) > 0) {
|
|
141
|
+
return "NothingToQuery";
|
|
142
|
+
} else {
|
|
143
|
+
return "WaitingForNewBlock";
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
136
147
|
async function checkAndFetch(crossChainState, dispatchChain) {
|
|
148
|
+
let targetBudget = crossChainState.targetBufferSize;
|
|
137
149
|
let remaining = {
|
|
138
|
-
contents: Primitive_float.max(0,
|
|
150
|
+
contents: Primitive_float.max(0, targetBudget - totalReadyCount(crossChainState) - totalReservedSize(crossChainState))
|
|
139
151
|
};
|
|
152
|
+
let minimumAdmissionBudget = targetBudget * 0.1;
|
|
153
|
+
let chunkItemsMultiplier = crossChainState.isRealtime ? 3 : 1.5;
|
|
154
|
+
let itemsTargetFloor = Primitive_int.div(crossChainState.targetBufferSize, FetchState.maxChainConcurrency);
|
|
155
|
+
let prioritizedChainStates = priorityOrder(crossChainState);
|
|
156
|
+
let alignment = crossChainState.isRealtime ? undefined : Stdlib_Option.map(prioritizedChainStates.find(cs => ChainState.knownHeight(cs) !== 0), cs => [
|
|
157
|
+
ChainState.chainConfig(cs).id,
|
|
158
|
+
ChainState.frontierProgress(cs)
|
|
159
|
+
]);
|
|
140
160
|
let actionByChain = {};
|
|
141
|
-
|
|
161
|
+
prioritizedChainStates.forEach(cs => {
|
|
142
162
|
let chainId = ChainState.chainConfig(cs).id;
|
|
143
|
-
|
|
144
|
-
|
|
163
|
+
if (ChainState.knownHeight(cs) === 0) {
|
|
164
|
+
actionByChain[chainId] = "WaitingForNewBlock";
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (remaining.contents < minimumAdmissionBudget) {
|
|
168
|
+
actionByChain[chainId] = "NothingToQuery";
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
let isCold = ChainState.effectiveDensity(cs) === undefined;
|
|
172
|
+
let chainTargetItems = (
|
|
173
|
+
isCold ? Primitive_float.min(remaining.contents, minimumAdmissionBudget) : remaining.contents
|
|
174
|
+
) + ChainState.pendingBudget(cs);
|
|
175
|
+
let maxTargetBlock = alignment !== undefined && alignment[0] !== chainId ? ChainState.blockAtProgress(cs, alignment[1] + 0.05) : undefined;
|
|
176
|
+
let action = ChainState.getNextQuery(cs, chainTargetItems, chunkItemsMultiplier, itemsTargetFloor, maxTargetBlock);
|
|
145
177
|
if (typeof action !== "object") {
|
|
146
178
|
if (action === "WaitingForNewBlock") {
|
|
147
179
|
actionByChain[chainId] = action;
|
|
148
180
|
return;
|
|
149
181
|
}
|
|
150
|
-
actionByChain[chainId] =
|
|
182
|
+
actionByChain[chainId] = idleOrWaitAction(cs);
|
|
151
183
|
return;
|
|
152
184
|
} else {
|
|
153
185
|
let queries = action._0;
|
|
154
|
-
let consumed = Stdlib_Array.reduce(queries, 0, (acc, query) => acc + query.
|
|
186
|
+
let consumed = Stdlib_Array.reduce(queries, 0, (acc, query) => acc + query.itemsEst);
|
|
155
187
|
let partitions = {};
|
|
156
188
|
queries.forEach(query => {
|
|
157
189
|
partitions[query.partitionId] = {
|
|
158
190
|
fromBlock: query.fromBlock,
|
|
159
191
|
targetBlock: query.toBlock,
|
|
160
|
-
targetEvents:
|
|
192
|
+
targetEvents: query.itemsTarget
|
|
161
193
|
};
|
|
162
194
|
});
|
|
163
195
|
Logging.trace({
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Owns all per-(effect, scope) runtime state and its lifecycle. The two maps
|
|
2
|
+
// have different rollback semantics, so keeping them together behind an explicit
|
|
3
|
+
// `resetForRollback` makes the invariant enforced rather than remembered.
|
|
4
|
+
|
|
5
|
+
// Per-scope rate-limit window and queue. A chain-scoped effect gets one of
|
|
6
|
+
// these per chain, so each chain's throughput is independent.
|
|
7
|
+
type effectRateLimitState = {
|
|
8
|
+
callsPerDuration: int,
|
|
9
|
+
durationMs: int,
|
|
10
|
+
mutable availableCalls: int,
|
|
11
|
+
mutable windowStartTime: float,
|
|
12
|
+
mutable queueCount: int,
|
|
13
|
+
mutable nextWindowPromise: option<promise<unit>>,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type effectCacheInMemTable = {
|
|
17
|
+
// Cache keys whose handler output is persisted on the next write. Drained
|
|
18
|
+
// each write; eviction is driven by the per-entry checkpointId instead.
|
|
19
|
+
mutable idsToStore: array<string>,
|
|
20
|
+
mutable invalidationsCount: int,
|
|
21
|
+
// Each entry is stamped with the checkpoint that referenced it (or
|
|
22
|
+
// loadedFromDbCheckpointId for db reads), so committed entries can be
|
|
23
|
+
// dropped once persisted/re-derivable, mirroring entity changes.
|
|
24
|
+
mutable dict: dict<Change.t<Internal.effectOutput>>,
|
|
25
|
+
mutable changesCount: float,
|
|
26
|
+
effect: Internal.effect,
|
|
27
|
+
// The scope this cache is for and its resolved db table (the address). The
|
|
28
|
+
// in-mem table is keyed by `table.tableName`, so a cross-chain and a
|
|
29
|
+
// chain-scoped cache for the same effect stay isolated.
|
|
30
|
+
scope: Internal.chainScope,
|
|
31
|
+
table: Table.table,
|
|
32
|
+
rateLimitState: option<effectRateLimitState>,
|
|
33
|
+
// Per-scope active-call bookkeeping, surfaced through the Effect metrics.
|
|
34
|
+
mutable activeCallsCount: int,
|
|
35
|
+
mutable prevCallStartTimerRef: Performance.timeRef,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type t = {
|
|
39
|
+
// Cache tables keyed by cache table name. Dropped on rollback — the cache
|
|
40
|
+
// is re-derivable from the db.
|
|
41
|
+
mutable tables: dict<effectCacheInMemTable>,
|
|
42
|
+
// Rate-limit windows keyed by the same name. Survive rollback: rate limiting
|
|
43
|
+
// reflects real API throughput, not indexing progress, so a reorg must not
|
|
44
|
+
// refill an effect's budget.
|
|
45
|
+
rateLimits: dict<effectRateLimitState>,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let make = (): t => {tables: Dict.make(), rateLimits: Dict.make()}
|
|
49
|
+
|
|
50
|
+
let getRateLimitState = (self: t, ~tableName, ~effect: Internal.effect) =>
|
|
51
|
+
switch effect.rateLimit {
|
|
52
|
+
| None => None
|
|
53
|
+
| Some({callsPerDuration, durationMs}) =>
|
|
54
|
+
Some(
|
|
55
|
+
switch self.rateLimits->Utils.Dict.dangerouslyGetNonOption(tableName) {
|
|
56
|
+
| Some(existing) => existing
|
|
57
|
+
| None =>
|
|
58
|
+
let created: effectRateLimitState = {
|
|
59
|
+
callsPerDuration,
|
|
60
|
+
durationMs,
|
|
61
|
+
availableCalls: callsPerDuration,
|
|
62
|
+
windowStartTime: Date.now(),
|
|
63
|
+
queueCount: 0,
|
|
64
|
+
nextWindowPromise: None,
|
|
65
|
+
}
|
|
66
|
+
self.rateLimits->Dict.set(tableName, created)
|
|
67
|
+
created
|
|
68
|
+
},
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Get, or lazily create, the in-mem table for an (effect, scope). A recreated
|
|
73
|
+
// table (e.g. after a rollback) reuses the surviving rate-limit window.
|
|
74
|
+
let getTable = (self: t, ~effect: Internal.effect, ~scope: Internal.chainScope) => {
|
|
75
|
+
let tableName = Internal.EffectCache.toTableName(~effectName=effect.name, ~scope)
|
|
76
|
+
switch self.tables->Utils.Dict.dangerouslyGetNonOption(tableName) {
|
|
77
|
+
| Some(inMemTable) => inMemTable
|
|
78
|
+
| None =>
|
|
79
|
+
let inMemTable: effectCacheInMemTable = {
|
|
80
|
+
idsToStore: [],
|
|
81
|
+
dict: Dict.make(),
|
|
82
|
+
changesCount: 0.,
|
|
83
|
+
invalidationsCount: 0,
|
|
84
|
+
effect,
|
|
85
|
+
scope,
|
|
86
|
+
table: Internal.makeCacheTable(~effectName=effect.name, ~scope),
|
|
87
|
+
rateLimitState: self->getRateLimitState(~tableName, ~effect),
|
|
88
|
+
activeCallsCount: 0,
|
|
89
|
+
prevCallStartTimerRef: %raw(`null`),
|
|
90
|
+
}
|
|
91
|
+
self.tables->Dict.set(tableName, inMemTable)
|
|
92
|
+
inMemTable
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let forEach = (self: t, fn) => self.tables->Utils.Dict.forEach(fn)
|
|
97
|
+
|
|
98
|
+
// Drop the cache tables (re-derivable from the db) but keep the rate-limit
|
|
99
|
+
// windows so a reorg doesn't hand an effect a fresh budget.
|
|
100
|
+
let resetForRollback = (self: t) => self.tables = Dict.make()
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Utils from "./Utils.res.mjs";
|
|
4
|
+
import * as Internal from "./Internal.res.mjs";
|
|
5
|
+
|
|
6
|
+
function make() {
|
|
7
|
+
return {
|
|
8
|
+
tables: {},
|
|
9
|
+
rateLimits: {}
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getRateLimitState(self, tableName, effect) {
|
|
14
|
+
let match = effect.rateLimit;
|
|
15
|
+
if (match === undefined) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
let callsPerDuration = match.callsPerDuration;
|
|
19
|
+
let existing = self.rateLimits[tableName];
|
|
20
|
+
let tmp;
|
|
21
|
+
if (existing !== undefined) {
|
|
22
|
+
tmp = existing;
|
|
23
|
+
} else {
|
|
24
|
+
let created = {
|
|
25
|
+
callsPerDuration: callsPerDuration,
|
|
26
|
+
durationMs: match.durationMs,
|
|
27
|
+
availableCalls: callsPerDuration,
|
|
28
|
+
windowStartTime: Date.now(),
|
|
29
|
+
queueCount: 0,
|
|
30
|
+
nextWindowPromise: undefined
|
|
31
|
+
};
|
|
32
|
+
self.rateLimits[tableName] = created;
|
|
33
|
+
tmp = created;
|
|
34
|
+
}
|
|
35
|
+
return tmp;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getTable(self, effect, scope) {
|
|
39
|
+
let tableName = Internal.EffectCache.toTableName(effect.name, scope);
|
|
40
|
+
let inMemTable = self.tables[tableName];
|
|
41
|
+
if (inMemTable !== undefined) {
|
|
42
|
+
return inMemTable;
|
|
43
|
+
}
|
|
44
|
+
let inMemTable$1 = {
|
|
45
|
+
idsToStore: [],
|
|
46
|
+
invalidationsCount: 0,
|
|
47
|
+
dict: {},
|
|
48
|
+
changesCount: 0,
|
|
49
|
+
effect: effect,
|
|
50
|
+
scope: scope,
|
|
51
|
+
table: Internal.makeCacheTable(effect.name, scope),
|
|
52
|
+
rateLimitState: getRateLimitState(self, tableName, effect),
|
|
53
|
+
activeCallsCount: 0,
|
|
54
|
+
prevCallStartTimerRef: null
|
|
55
|
+
};
|
|
56
|
+
self.tables[tableName] = inMemTable$1;
|
|
57
|
+
return inMemTable$1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function forEach(self, fn) {
|
|
61
|
+
Utils.Dict.forEach(self.tables, fn);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function resetForRollback(self) {
|
|
65
|
+
self.tables = {};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export {
|
|
69
|
+
make,
|
|
70
|
+
getTable,
|
|
71
|
+
forEach,
|
|
72
|
+
resetForRollback,
|
|
73
|
+
}
|
|
74
|
+
/* Utils Not a pure module */
|