envio 3.3.0-alpha.7 → 3.3.0-alpha.9
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 +23 -5
- package/package.json +6 -6
- package/src/Api.res +1 -1
- package/src/Api.res.mjs +1 -1
- package/src/ChainFetching.res +21 -10
- package/src/ChainFetching.res.mjs +11 -10
- package/src/ChainState.res +401 -171
- package/src/ChainState.res.mjs +270 -104
- package/src/ChainState.resi +19 -7
- package/src/Config.res +11 -27
- package/src/Config.res.mjs +8 -7
- package/src/Core.res +7 -0
- package/src/CrossChainState.res +82 -82
- package/src/CrossChainState.res.mjs +52 -59
- package/src/CrossChainState.resi +1 -1
- package/src/Ecosystem.res +3 -3
- package/src/Ecosystem.res.mjs +3 -3
- package/src/Envio.res +14 -9
- package/src/EnvioGlobal.res +53 -0
- package/src/EnvioGlobal.res.mjs +31 -0
- package/src/EventConfigBuilder.res +117 -69
- package/src/EventConfigBuilder.res.mjs +71 -34
- package/src/EventProcessing.res +19 -15
- package/src/EventProcessing.res.mjs +13 -12
- package/src/FetchState.res +410 -185
- package/src/FetchState.res.mjs +268 -253
- package/src/HandlerLoader.res +8 -113
- package/src/HandlerLoader.res.mjs +2 -88
- package/src/HandlerRegister.res +518 -144
- package/src/HandlerRegister.res.mjs +285 -133
- package/src/HandlerRegister.resi +24 -8
- package/src/IndexerState.res +6 -3
- package/src/IndexerState.res.mjs +3 -3
- package/src/IndexerState.resi +1 -1
- package/src/IndexingAddresses.res +10 -7
- package/src/IndexingAddresses.res.mjs +8 -7
- package/src/IndexingAddresses.resi +3 -1
- package/src/Internal.res +104 -39
- package/src/Internal.res.mjs +11 -1
- package/src/LogSelection.res +102 -165
- package/src/LogSelection.res.mjs +101 -116
- package/src/Main.res +49 -164
- package/src/Main.res.mjs +39 -104
- package/src/Metrics.res +43 -2
- package/src/Metrics.res.mjs +39 -3
- package/src/Prometheus.res +0 -35
- package/src/Prometheus.res.mjs +33 -68
- package/src/RawEvent.res +7 -2
- package/src/RawEvent.res.mjs +4 -4
- package/src/RollbackCommit.res +4 -1
- package/src/RollbackCommit.res.mjs +3 -2
- package/src/SimulateItems.res +20 -7
- package/src/SimulateItems.res.mjs +7 -11
- package/src/TestIndexer.res +1 -1
- package/src/TestIndexer.res.mjs +1 -1
- package/src/sources/BlockStore.res +46 -0
- package/src/sources/BlockStore.res.mjs +24 -0
- package/src/sources/EventRouter.res +19 -12
- package/src/sources/EventRouter.res.mjs +7 -5
- package/src/sources/Evm.res +58 -5
- package/src/sources/Evm.res.mjs +44 -5
- package/src/sources/EvmChain.res +14 -18
- package/src/sources/EvmChain.res.mjs +12 -13
- package/src/sources/FieldMask.res +39 -0
- package/src/sources/FieldMask.res.mjs +42 -0
- package/src/sources/Fuel.res +8 -3
- package/src/sources/Fuel.res.mjs +5 -4
- package/src/sources/HyperFuelSource.res +31 -40
- package/src/sources/HyperFuelSource.res.mjs +52 -44
- package/src/sources/HyperSync.res +31 -9
- package/src/sources/HyperSync.res.mjs +47 -31
- package/src/sources/HyperSync.resi +10 -3
- package/src/sources/HyperSyncClient.res +15 -4
- package/src/sources/HyperSyncHeightStream.res +1 -8
- package/src/sources/HyperSyncHeightStream.res.mjs +0 -2
- package/src/sources/HyperSyncSource.res +58 -90
- package/src/sources/HyperSyncSource.res.mjs +57 -66
- package/src/sources/RpcSource.res +118 -120
- package/src/sources/RpcSource.res.mjs +92 -74
- package/src/sources/RpcWebSocketHeightStream.res +0 -5
- package/src/sources/RpcWebSocketHeightStream.res.mjs +0 -2
- package/src/sources/SimulateSource.res +5 -3
- package/src/sources/SimulateSource.res.mjs +12 -4
- package/src/sources/Source.res +19 -5
- package/src/sources/SourceManager.res +60 -10
- package/src/sources/SourceManager.res.mjs +54 -8
- package/src/sources/SourceManager.resi +10 -0
- package/src/sources/Svm.res +14 -10
- package/src/sources/Svm.res.mjs +23 -6
- package/src/sources/SvmHyperSyncClient.res +5 -6
- package/src/sources/SvmHyperSyncSource.res +82 -41
- package/src/sources/SvmHyperSyncSource.res.mjs +86 -41
- package/src/sources/TransactionStore.res +6 -107
- package/src/sources/TransactionStore.res.mjs +5 -86
- package/svm.schema.json +19 -0
package/src/FetchState.res
CHANGED
|
@@ -7,15 +7,19 @@ type blockNumberAndTimestamp = {
|
|
|
7
7
|
|
|
8
8
|
type blockNumberAndLogIndex = {blockNumber: int, logIndex: int}
|
|
9
9
|
|
|
10
|
-
type selection = {
|
|
10
|
+
type selection = {
|
|
11
|
+
onEventRegistrations: array<Internal.onEventRegistration>,
|
|
12
|
+
dependsOnAddresses: bool,
|
|
13
|
+
}
|
|
11
14
|
|
|
12
15
|
type pendingQuery = {
|
|
13
16
|
fromBlock: int,
|
|
14
17
|
toBlock: option<int>,
|
|
15
18
|
isChunk: bool,
|
|
16
|
-
//
|
|
17
|
-
// shared buffer budget can account for what's already
|
|
18
|
-
|
|
19
|
+
// Items this in-flight query is targeting (server maxNumLogs-style cap), carried
|
|
20
|
+
// from the query so the shared buffer budget can account for what's already
|
|
21
|
+
// being fetched.
|
|
22
|
+
itemsTarget: int,
|
|
19
23
|
// Stores latestFetchedBlock when query completes. Only needed to persist
|
|
20
24
|
// timestamp while earlier queries are still pending before updating
|
|
21
25
|
// the partition's latestFetchedBlock.
|
|
@@ -59,15 +63,12 @@ type query = {
|
|
|
59
63
|
fromBlock: int,
|
|
60
64
|
toBlock: option<int>,
|
|
61
65
|
isChunk: bool,
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
// (furthest-behind first) without allocating a side tuple per query.
|
|
69
|
-
mutable chainId: int,
|
|
70
|
-
mutable progress: float,
|
|
66
|
+
// Items this query targets: the server-side maxNumLogs-style cap, and the
|
|
67
|
+
// unit the chain's per-tick budget is reserved/consumed in. For a partition
|
|
68
|
+
// with known density this is density × the query's block range; for a
|
|
69
|
+
// partition with no signal yet it's whatever budget share the query was
|
|
70
|
+
// sized against.
|
|
71
|
+
itemsTarget: int,
|
|
71
72
|
selection: selection,
|
|
72
73
|
addressesByContractName: dict<array<Address.t>>,
|
|
73
74
|
}
|
|
@@ -90,42 +91,20 @@ let deriveContractNameByAddress: dict<array<Address.t>> => dict<
|
|
|
90
91
|
result
|
|
91
92
|
})
|
|
92
93
|
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
// free. Scaled down as partitionsCount grows: assuming every partition's first
|
|
101
|
-
// query is "big" starves the rest of that tick's admission when many
|
|
102
|
-
// partitions share the same buffer, so the per-partition default shrinks
|
|
103
|
-
// accordingly — clamped to [minEstResponseSize, maxEstResponseSize] either way.
|
|
104
|
-
let calculateDefaultEstResponseSize = (~partitionsCount) =>
|
|
94
|
+
// itemsTarget for a query over [fromBlock, toBlock] at the given event density
|
|
95
|
+
// (items/block). toBlock None is the open-ended tail, capped at
|
|
96
|
+
// chainTargetBlock — the soft per-tick horizon the owning chain wants to reach
|
|
97
|
+
// (see getNextQuery).
|
|
98
|
+
let densityItemsTarget = (~density, ~fromBlock, ~toBlock, ~chainTargetBlock) => {
|
|
99
|
+
// Floor at 1: the reservation must equal the server-side cap SourceManager
|
|
100
|
+
// sends, and a 0 cap would ask the backend for nothing.
|
|
105
101
|
Pervasives.max(
|
|
106
|
-
|
|
107
|
-
|
|
102
|
+
1,
|
|
103
|
+
((toBlock->Option.getOr(chainTargetBlock) - fromBlock + 1)->Int.toFloat *. density)
|
|
104
|
+
->Math.ceil
|
|
105
|
+
->Float.toInt,
|
|
108
106
|
)
|
|
109
|
-
|
|
110
|
-
// Estimated items a query will return, from the partition's event density
|
|
111
|
-
// (items/block derived from its last response) and the query's block range.
|
|
112
|
-
// toBlock None is the open-ended tail, capped at knownHeight. A partition
|
|
113
|
-
// that responded with no items has density 0, so its queries cost 0 — correct,
|
|
114
|
-
// they don't fill the buffer. Only a partition that has never responded
|
|
115
|
-
// (prevQueryRange 0) has no signal, so it falls back to calculateDefaultEstResponseSize.
|
|
116
|
-
let calculateEstResponseSize = (
|
|
117
|
-
p: partition,
|
|
118
|
-
~fromBlock,
|
|
119
|
-
~toBlock,
|
|
120
|
-
~knownHeight,
|
|
121
|
-
~partitionsCount,
|
|
122
|
-
) =>
|
|
123
|
-
if p.prevQueryRange > 0 {
|
|
124
|
-
let density = p.prevRangeSize->Int.toFloat /. p.prevQueryRange->Int.toFloat
|
|
125
|
-
(toBlock->Option.getOr(knownHeight) - fromBlock + 1)->Int.toFloat *. density
|
|
126
|
-
} else {
|
|
127
|
-
calculateDefaultEstResponseSize(~partitionsCount)
|
|
128
|
-
}
|
|
107
|
+
}
|
|
129
108
|
|
|
130
109
|
// Calculate the chunk range from history using min-of-last-3-ranges heuristic
|
|
131
110
|
let getMinHistoryRange = (p: partition) => {
|
|
@@ -135,6 +114,15 @@ let getMinHistoryRange = (p: partition) => {
|
|
|
135
114
|
}
|
|
136
115
|
}
|
|
137
116
|
|
|
117
|
+
// Density (items/block) from the last response, trusted only after two
|
|
118
|
+
// responses — a single sample is too noisy to size the next query by.
|
|
119
|
+
let getTrustedDensity = (p: partition) => {
|
|
120
|
+
switch (p.prevQueryRange, p.prevPrevQueryRange) {
|
|
121
|
+
| (0, _) | (_, 0) => None
|
|
122
|
+
| (prevQueryRange, _) => Some(p.prevRangeSize->Int.toFloat /. prevQueryRange->Int.toFloat)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
138
126
|
let getMinQueryRange = (partitions: array<partition>) => {
|
|
139
127
|
let min = ref(0)
|
|
140
128
|
for i in 0 to partitions->Array.length - 1 {
|
|
@@ -214,6 +202,12 @@ module OptimizedPartitions = {
|
|
|
214
202
|
let newId = nextPartitionIndexRef.contents->Int.toString
|
|
215
203
|
nextPartitionIndexRef := nextPartitionIndexRef.contents + 1
|
|
216
204
|
let minRange = getMinQueryRange([p1, p2])
|
|
205
|
+
// The merged partition indexes both parents' addresses, so its expected
|
|
206
|
+
// event rate is the sum of their densities. Parents without a trusted
|
|
207
|
+
// density contribute 0; if none has one, prevRangeSize stays 0 and the
|
|
208
|
+
// partition probes for a fresh signal instead of chunking.
|
|
209
|
+
let inheritedDensity =
|
|
210
|
+
p1->getTrustedDensity->Option.getOr(0.) +. p2->getTrustedDensity->Option.getOr(0.)
|
|
217
211
|
{
|
|
218
212
|
id: newId,
|
|
219
213
|
dynamicContract: Some(contractName),
|
|
@@ -224,7 +218,7 @@ module OptimizedPartitions = {
|
|
|
224
218
|
mutPendingQueries: [],
|
|
225
219
|
prevQueryRange: minRange,
|
|
226
220
|
prevPrevQueryRange: minRange,
|
|
227
|
-
prevRangeSize:
|
|
221
|
+
prevRangeSize: (inheritedDensity *. minRange->Int.toFloat)->Math.ceil->Float.toInt,
|
|
228
222
|
latestBlockRangeUpdateBlock: 0,
|
|
229
223
|
}
|
|
230
224
|
}
|
|
@@ -566,7 +560,7 @@ type t = {
|
|
|
566
560
|
// size). Event fetch depth is bounded separately, by CrossChainState's
|
|
567
561
|
// cross-chain admission against the indexer-wide buffer pool.
|
|
568
562
|
maxOnBlockBufferSize: int,
|
|
569
|
-
|
|
563
|
+
onBlockRegistrations: array<Internal.onBlockRegistration>,
|
|
570
564
|
knownHeight: int,
|
|
571
565
|
firstEventBlock: option<int>,
|
|
572
566
|
}
|
|
@@ -642,7 +636,7 @@ let blockItemLogIndex = 16777216
|
|
|
642
636
|
// are generated at once to prevent OOM.
|
|
643
637
|
let appendOnBlockItems = (
|
|
644
638
|
~mutItems: array<Internal.item>,
|
|
645
|
-
~
|
|
639
|
+
~onBlockRegistrations: array<Internal.onBlockRegistration>,
|
|
646
640
|
~indexerStartBlock,
|
|
647
641
|
~fromBlock,
|
|
648
642
|
~maxBlockNumber,
|
|
@@ -662,27 +656,27 @@ let appendOnBlockItems = (
|
|
|
662
656
|
let blockNumber = latestOnBlockBlockNumber.contents + 1
|
|
663
657
|
latestOnBlockBlockNumber := blockNumber
|
|
664
658
|
|
|
665
|
-
for configIdx in 0 to
|
|
666
|
-
let
|
|
659
|
+
for configIdx in 0 to onBlockRegistrations->Array.length - 1 {
|
|
660
|
+
let onBlockRegistration = onBlockRegistrations->Array.getUnsafe(configIdx)
|
|
667
661
|
|
|
668
|
-
let handlerStartBlock = switch
|
|
662
|
+
let handlerStartBlock = switch onBlockRegistration.startBlock {
|
|
669
663
|
| Some(startBlock) => startBlock
|
|
670
664
|
| None => indexerStartBlock
|
|
671
665
|
}
|
|
672
666
|
|
|
673
667
|
if (
|
|
674
668
|
blockNumber >= handlerStartBlock &&
|
|
675
|
-
switch
|
|
669
|
+
switch onBlockRegistration.endBlock {
|
|
676
670
|
| Some(endBlock) => blockNumber <= endBlock
|
|
677
671
|
| None => true
|
|
678
672
|
} &&
|
|
679
|
-
(blockNumber - handlerStartBlock)->Pervasives.mod(
|
|
673
|
+
(blockNumber - handlerStartBlock)->Pervasives.mod(onBlockRegistration.interval) === 0
|
|
680
674
|
) {
|
|
681
675
|
mutItems->Array.push(
|
|
682
676
|
Block({
|
|
683
|
-
|
|
677
|
+
onBlockRegistration,
|
|
684
678
|
blockNumber,
|
|
685
|
-
logIndex: blockItemLogIndex +
|
|
679
|
+
logIndex: blockItemLogIndex + onBlockRegistration.index,
|
|
686
680
|
}),
|
|
687
681
|
)
|
|
688
682
|
newItemsCounter := newItemsCounter.contents + 1
|
|
@@ -706,9 +700,9 @@ let updateInternal = (
|
|
|
706
700
|
): t => {
|
|
707
701
|
let mutItemsRef = ref(mutItems)
|
|
708
702
|
|
|
709
|
-
let latestOnBlockBlockNumber = switch fetchState.
|
|
703
|
+
let latestOnBlockBlockNumber = switch fetchState.onBlockRegistrations {
|
|
710
704
|
| [] => knownHeight
|
|
711
|
-
|
|
|
705
|
+
| onBlockRegistrations => {
|
|
712
706
|
// Calculate the max block number we are going to create items for
|
|
713
707
|
// Use maxOnBlockBufferSize to get the last target item in the buffer
|
|
714
708
|
//
|
|
@@ -736,7 +730,7 @@ let updateInternal = (
|
|
|
736
730
|
|
|
737
731
|
appendOnBlockItems(
|
|
738
732
|
~mutItems,
|
|
739
|
-
~
|
|
733
|
+
~onBlockRegistrations,
|
|
740
734
|
~indexerStartBlock=fetchState.startBlock,
|
|
741
735
|
~fromBlock=fetchState.latestOnBlockBlockNumber,
|
|
742
736
|
~maxBlockNumber,
|
|
@@ -751,7 +745,7 @@ let updateInternal = (
|
|
|
751
745
|
contractConfigs: fetchState.contractConfigs,
|
|
752
746
|
normalSelection: fetchState.normalSelection,
|
|
753
747
|
chainId: fetchState.chainId,
|
|
754
|
-
|
|
748
|
+
onBlockRegistrations: fetchState.onBlockRegistrations,
|
|
755
749
|
maxOnBlockBufferSize: fetchState.maxOnBlockBufferSize,
|
|
756
750
|
optimizedPartitions,
|
|
757
751
|
latestOnBlockBlockNumber,
|
|
@@ -1017,7 +1011,7 @@ let registerDynamicContracts = (
|
|
|
1017
1011
|
// Might contain duplicates which we should filter out
|
|
1018
1012
|
items: array<Internal.item>,
|
|
1019
1013
|
) => {
|
|
1020
|
-
if fetchState.normalSelection.
|
|
1014
|
+
if fetchState.normalSelection.onEventRegistrations->Utils.Array.isEmpty {
|
|
1021
1015
|
// Can the normalSelection be empty?
|
|
1022
1016
|
JsError.throwWithMessage(
|
|
1023
1017
|
"Invalid configuration. No events to fetch for the dynamic contract registration.",
|
|
@@ -1301,8 +1295,8 @@ let handleQueryResult = (
|
|
|
1301
1295
|
// param-level analogue of EventRouter's srcAddress effectiveStartBlock check.
|
|
1302
1296
|
let newItems = newItems->Array.filter(item =>
|
|
1303
1297
|
switch item {
|
|
1304
|
-
| Internal.Event({
|
|
1305
|
-
switch
|
|
1298
|
+
| Internal.Event({onEventRegistration, payload, blockNumber}) =>
|
|
1299
|
+
switch onEventRegistration.clientAddressFilter {
|
|
1306
1300
|
| Some(filter) =>
|
|
1307
1301
|
filter(payload, blockNumber, indexingAddresses->IndexingAddresses.rawForFilter)
|
|
1308
1302
|
| None => true
|
|
@@ -1340,7 +1334,7 @@ let startFetchingQueries = ({optimizedPartitions}: t, ~queries: array<query>) =>
|
|
|
1340
1334
|
fromBlock: q.fromBlock,
|
|
1341
1335
|
toBlock: q.toBlock,
|
|
1342
1336
|
isChunk: q.isChunk,
|
|
1343
|
-
|
|
1337
|
+
itemsTarget: q.itemsTarget,
|
|
1344
1338
|
fetchedBlock: None,
|
|
1345
1339
|
}
|
|
1346
1340
|
|
|
@@ -1364,48 +1358,58 @@ let startFetchingQueries = ({optimizedPartitions}: t, ~queries: array<query>) =>
|
|
|
1364
1358
|
// Most parallel in-flight chunk queries a single partition may have at once.
|
|
1365
1359
|
let maxPendingChunksPerPartition = 10
|
|
1366
1360
|
|
|
1367
|
-
|
|
1368
|
-
|
|
1361
|
+
// Fills a gap range (a hole left between completed/pending chunks, e.g. from an
|
|
1362
|
+
// out-of-order partial response) unconditionally — gaps are already-committed
|
|
1363
|
+
// range, not subject to this tick's water-fill budget. Priced by the
|
|
1364
|
+
// partition's trusted density when it has one; otherwise by the "available
|
|
1365
|
+
// density" — the partition's equal-divide budget spread over its remaining
|
|
1366
|
+
// range this tick — so a small gap reserves proportionally little instead of a
|
|
1367
|
+
// noisy one-sample estimate. Chunks only on a trusted POSITIVE density, same
|
|
1368
|
+
// rule as the water-fill. Returns the created queries' total itemsTarget.
|
|
1369
|
+
let pushGapFillQueries = (
|
|
1369
1370
|
queries: array<query>,
|
|
1370
1371
|
~partitionId: string,
|
|
1371
1372
|
~rangeFromBlock: int,
|
|
1372
1373
|
~rangeEndBlock: option<int>,
|
|
1373
1374
|
~knownHeight: int,
|
|
1375
|
+
~chainTargetBlock: int,
|
|
1374
1376
|
~maybeChunkRange: option<int>,
|
|
1375
1377
|
~maxChunks: int,
|
|
1376
1378
|
~partition: partition,
|
|
1379
|
+
~partitionBudget: float,
|
|
1377
1380
|
~selection: selection,
|
|
1378
1381
|
~addressesByContractName: dict<array<Address.t>>,
|
|
1379
|
-
~partitionsCount: int,
|
|
1380
1382
|
) => {
|
|
1383
|
+
let cost = ref(0.)
|
|
1381
1384
|
if rangeFromBlock <= knownHeight && maxChunks > 0 {
|
|
1382
1385
|
switch rangeEndBlock {
|
|
1383
1386
|
| Some(endBlock) if rangeFromBlock > endBlock => ()
|
|
1384
1387
|
| _ =>
|
|
1385
|
-
|
|
1386
|
-
|
|
1388
|
+
let trustedDensity = partition->getTrustedDensity
|
|
1389
|
+
let maxBlock = switch rangeEndBlock {
|
|
1390
|
+
| Some(eb) => eb
|
|
1391
|
+
| None => chainTargetBlock
|
|
1392
|
+
}
|
|
1393
|
+
let pushSingleQuery = (~density, ~isChunk) => {
|
|
1394
|
+
let itemsTarget = densityItemsTarget(
|
|
1395
|
+
~density,
|
|
1396
|
+
~fromBlock=rangeFromBlock,
|
|
1397
|
+
~toBlock=rangeEndBlock,
|
|
1398
|
+
~chainTargetBlock,
|
|
1399
|
+
)
|
|
1387
1400
|
queries->Array.push({
|
|
1388
1401
|
partitionId,
|
|
1389
1402
|
fromBlock: rangeFromBlock,
|
|
1390
1403
|
toBlock: rangeEndBlock,
|
|
1391
1404
|
selection,
|
|
1392
|
-
isChunk
|
|
1393
|
-
|
|
1394
|
-
partition,
|
|
1395
|
-
~fromBlock=rangeFromBlock,
|
|
1396
|
-
~toBlock=rangeEndBlock,
|
|
1397
|
-
~knownHeight,
|
|
1398
|
-
~partitionsCount,
|
|
1399
|
-
),
|
|
1400
|
-
chainId: 0,
|
|
1401
|
-
progress: 0.,
|
|
1405
|
+
isChunk,
|
|
1406
|
+
itemsTarget,
|
|
1402
1407
|
addressesByContractName,
|
|
1403
1408
|
})
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
}
|
|
1409
|
+
cost := cost.contents +. itemsTarget->Int.toFloat
|
|
1410
|
+
}
|
|
1411
|
+
switch (trustedDensity, maybeChunkRange) {
|
|
1412
|
+
| (Some(density), Some(chunkRange)) if density > 0. =>
|
|
1409
1413
|
let chunkSize = Js.Math.ceil_int(chunkRange->Int.toFloat *. 1.8)
|
|
1410
1414
|
if rangeFromBlock + chunkSize * 2 - 1 <= maxBlock {
|
|
1411
1415
|
let chunkFromBlock = ref(rangeFromBlock)
|
|
@@ -1414,58 +1418,111 @@ let pushQueriesForRange = (
|
|
|
1414
1418
|
chunkIdx.contents < maxChunks && chunkFromBlock.contents + chunkSize - 1 <= maxBlock
|
|
1415
1419
|
) {
|
|
1416
1420
|
let chunkToBlock = chunkFromBlock.contents + chunkSize - 1
|
|
1421
|
+
let itemsTarget = densityItemsTarget(
|
|
1422
|
+
~density,
|
|
1423
|
+
~fromBlock=chunkFromBlock.contents,
|
|
1424
|
+
~toBlock=Some(chunkToBlock),
|
|
1425
|
+
~chainTargetBlock,
|
|
1426
|
+
)
|
|
1417
1427
|
queries->Array.push({
|
|
1418
1428
|
partitionId,
|
|
1419
1429
|
fromBlock: chunkFromBlock.contents,
|
|
1420
1430
|
toBlock: Some(chunkToBlock),
|
|
1421
1431
|
isChunk: true,
|
|
1422
1432
|
selection,
|
|
1423
|
-
|
|
1424
|
-
partition,
|
|
1425
|
-
~fromBlock=chunkFromBlock.contents,
|
|
1426
|
-
~toBlock=Some(chunkToBlock),
|
|
1427
|
-
~knownHeight,
|
|
1428
|
-
~partitionsCount,
|
|
1429
|
-
),
|
|
1430
|
-
chainId: 0,
|
|
1431
|
-
progress: 0.,
|
|
1433
|
+
itemsTarget,
|
|
1432
1434
|
addressesByContractName,
|
|
1433
1435
|
})
|
|
1436
|
+
cost := cost.contents +. itemsTarget->Int.toFloat
|
|
1434
1437
|
chunkFromBlock := chunkToBlock + 1
|
|
1435
1438
|
chunkIdx := chunkIdx.contents + 1
|
|
1436
1439
|
}
|
|
1437
1440
|
} else {
|
|
1438
1441
|
// Not enough room for 2 chunks, fall back to a single query
|
|
1439
|
-
|
|
1440
|
-
partitionId,
|
|
1441
|
-
fromBlock: rangeFromBlock,
|
|
1442
|
-
toBlock: rangeEndBlock,
|
|
1443
|
-
selection,
|
|
1444
|
-
isChunk: rangeEndBlock !== None,
|
|
1445
|
-
estResponseSize: calculateEstResponseSize(
|
|
1446
|
-
partition,
|
|
1447
|
-
~fromBlock=rangeFromBlock,
|
|
1448
|
-
~toBlock=rangeEndBlock,
|
|
1449
|
-
~knownHeight,
|
|
1450
|
-
~partitionsCount,
|
|
1451
|
-
),
|
|
1452
|
-
chainId: 0,
|
|
1453
|
-
progress: 0.,
|
|
1454
|
-
addressesByContractName,
|
|
1455
|
-
})
|
|
1442
|
+
pushSingleQuery(~density, ~isChunk=rangeEndBlock !== None)
|
|
1456
1443
|
}
|
|
1444
|
+
| (Some(density), _) => pushSingleQuery(~density, ~isChunk=false)
|
|
1445
|
+
| (None, _) =>
|
|
1446
|
+
let remainingRange = Pervasives.max(1, chainTargetBlock - rangeFromBlock + 1)
|
|
1447
|
+
pushSingleQuery(~density=partitionBudget /. remainingRange->Int.toFloat, ~isChunk=false)
|
|
1457
1448
|
}
|
|
1458
1449
|
}
|
|
1459
1450
|
}
|
|
1451
|
+
cost.contents
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
// The level every partition ends at when `budget` is poured across partitions
|
|
1455
|
+
// already holding `footprints`: the unique L with Σ max(0, L - fᵢ) = budget.
|
|
1456
|
+
// Partitions above L get nothing (their head start is their share); the rest
|
|
1457
|
+
// are topped up exactly to L, so the pour equals the budget no matter how
|
|
1458
|
+
// uneven the footprints are.
|
|
1459
|
+
let waterLevel = (~budget: float, ~footprints: array<float>) => {
|
|
1460
|
+
let sorted = footprints->Array.toSorted(Float.compare)
|
|
1461
|
+
let n = sorted->Array.length
|
|
1462
|
+
let prefix = ref(0.)
|
|
1463
|
+
let level = ref(None)
|
|
1464
|
+
let idx = ref(0)
|
|
1465
|
+
while level.contents == None && idx.contents < n {
|
|
1466
|
+
let i = idx.contents
|
|
1467
|
+
prefix := prefix.contents +. sorted->Array.getUnsafe(i)
|
|
1468
|
+
// The level if only the i+1 lowest footprints receive water — correct once
|
|
1469
|
+
// it doesn't reach the next footprint up.
|
|
1470
|
+
let candidate = (budget +. prefix.contents) /. (i + 1)->Int.toFloat
|
|
1471
|
+
if i == n - 1 || candidate <= sorted->Array.getUnsafe(i + 1) {
|
|
1472
|
+
level := Some(candidate)
|
|
1473
|
+
}
|
|
1474
|
+
idx := idx.contents + 1
|
|
1475
|
+
}
|
|
1476
|
+
level.contents->Option.getOr(0.)
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
// Per-partition mutable state threaded through the water-fill rounds below.
|
|
1480
|
+
type waterFillState = {
|
|
1481
|
+
partitionId: string,
|
|
1482
|
+
p: partition,
|
|
1483
|
+
mutable cursor: int,
|
|
1484
|
+
mutable chunksUsedThisCall: int,
|
|
1485
|
+
// Existing mutPendingQueries count before this call — fixed for the call,
|
|
1486
|
+
// used with chunksUsedThisCall against maxPendingChunksPerPartition.
|
|
1487
|
+
pendingCount: int,
|
|
1488
|
+
queryEndBlock: option<int>,
|
|
1489
|
+
maybeChunkRange: option<int>,
|
|
1490
|
+
// This partition's own slot in queriesByPartitionIndex — gap-fill and every
|
|
1491
|
+
// water-fill round push here directly, so query order falls out of
|
|
1492
|
+
// idsInAscOrder for free (see getNextQuery) instead of a final sort pass.
|
|
1493
|
+
bucket: array<query>,
|
|
1460
1494
|
}
|
|
1461
1495
|
|
|
1462
|
-
// Candidate queries are sized against the
|
|
1463
|
-
//
|
|
1464
|
-
//
|
|
1465
|
-
//
|
|
1466
|
-
//
|
|
1496
|
+
// Candidate queries are sized against chainTargetBlock, the soft querying
|
|
1497
|
+
// horizon the owning chain wants to reach this tick — derived by ChainState
|
|
1498
|
+
// from its share of the indexer-wide buffer budget and its chain-level event
|
|
1499
|
+
// density. chainTargetBlock is never used as a hard query end, only to (a)
|
|
1500
|
+
// select which partitions are "in range" this tick and (b) size an open-ended
|
|
1501
|
+
// query with no other ceiling: the true hard bounds stay
|
|
1502
|
+
// endBlock/mergeBlock/the lagged head.
|
|
1503
|
+
//
|
|
1504
|
+
// In-range partitions share chainTargetItems (this chain's target total
|
|
1505
|
+
// footprint — in-flight plus new) by water-fill: each round pours exactly the
|
|
1506
|
+
// remaining fresh budget at the level computed by waterLevel, so a partition
|
|
1507
|
+
// already holding more than the level (from earlier ticks' in-flight queries)
|
|
1508
|
+
// gets nothing and its implicit share flows to the others, while totals stay
|
|
1509
|
+
// even and the pour never exceeds the budget. Rounds repeat only because
|
|
1510
|
+
// emits are quantized: a partition may consume less than its allotment (range
|
|
1511
|
+
// or chunk-cap runs out) or slightly more (min one chunk), and the leftover
|
|
1512
|
+
// re-pours over whoever still has range left.
|
|
1513
|
+
//
|
|
1514
|
+
// A partition with a trusted positive density (two or more responses — see
|
|
1515
|
+
// getMinHistoryRange) always emits at least one full-size chunk/query once
|
|
1516
|
+
// given an allotment, sized by its real density — may overshoot the
|
|
1517
|
+
// allotment by at most one chunk (the server also enforces itemsTarget via a
|
|
1518
|
+
// maxNumLogs-style cap, so an overshoot truncates the response rather than
|
|
1519
|
+
// the buffer). Any other partition (no signal, one noisy sample, or a
|
|
1520
|
+
// density-0 estimate) emits one open-ended probe sized exactly at its
|
|
1521
|
+
// allotment instead.
|
|
1467
1522
|
let getNextQuery = (
|
|
1468
1523
|
{optimizedPartitions, blockLag, latestOnBlockBlockNumber, knownHeight, endBlock}: t,
|
|
1524
|
+
~chainTargetBlock: int,
|
|
1525
|
+
~chainTargetItems: float,
|
|
1469
1526
|
) => {
|
|
1470
1527
|
let headBlockNumber = knownHeight - blockLag
|
|
1471
1528
|
if headBlockNumber <= 0 {
|
|
@@ -1480,20 +1537,17 @@ let getNextQuery = (
|
|
|
1480
1537
|
!isOnBlockBehindTheHead,
|
|
1481
1538
|
)
|
|
1482
1539
|
|
|
1483
|
-
let queries = []
|
|
1484
|
-
|
|
1485
1540
|
let partitionsCount = optimizedPartitions.idsInAscOrder->Array.length
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1541
|
+
|
|
1542
|
+
// Every partition is visited once here regardless of whether it gets a
|
|
1543
|
+
// query pushed below — waiting-for-new-block bookkeeping shouldn't depend
|
|
1544
|
+
// on this tick's budget.
|
|
1545
|
+
for idx in 0 to partitionsCount - 1 {
|
|
1489
1546
|
let partitionId = optimizedPartitions.idsInAscOrder->Array.getUnsafe(idx)
|
|
1490
1547
|
let p = optimizedPartitions.entities->Dict.getUnsafe(partitionId)
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
let hasPendingQueries = pendingCount > 0
|
|
1495
|
-
|
|
1496
|
-
if hasPendingQueries || isBehindTheHead {
|
|
1548
|
+
if (
|
|
1549
|
+
p.mutPendingQueries->Array.length > 0 || p.latestFetchedBlock.blockNumber < headBlockNumber
|
|
1550
|
+
) {
|
|
1497
1551
|
// Even if there are some partitions waiting for the new block
|
|
1498
1552
|
// We still want to wait for all partitions reaching the head
|
|
1499
1553
|
// because they might update knownHeight in their response
|
|
@@ -1501,46 +1555,86 @@ let getNextQuery = (
|
|
|
1501
1555
|
// and we don't want to poll the head for a few small partitions
|
|
1502
1556
|
shouldWaitForNewBlock := false
|
|
1503
1557
|
}
|
|
1558
|
+
}
|
|
1504
1559
|
|
|
1505
|
-
|
|
1560
|
+
// One bucket per partition, in idsInAscOrder order — gap-fill and
|
|
1561
|
+
// water-fill both push into a partition's own bucket, so flattening at
|
|
1562
|
+
// the end (see below) reproduces idsInAscOrder without a sort.
|
|
1563
|
+
let queriesByPartitionIndex: array<
|
|
1564
|
+
array<query>,
|
|
1565
|
+
> = Array.fromInitializer(~length=partitionsCount, _ => [])
|
|
1506
1566
|
|
|
1507
|
-
|
|
1567
|
+
// Compute queryEndBlock for this partition
|
|
1568
|
+
let computeQueryEndBlock = (p: partition) => {
|
|
1508
1569
|
let queryEndBlock = Utils.Math.minOptInt(endBlock, p.mergeBlock)
|
|
1509
|
-
|
|
1570
|
+
switch blockLag {
|
|
1510
1571
|
| 0 => queryEndBlock
|
|
1511
1572
|
| _ =>
|
|
1512
1573
|
// Force head block as an endBlock when blockLag is set
|
|
1513
1574
|
// because otherwise HyperSync might return bigger range
|
|
1514
1575
|
Utils.Math.minOptInt(Some(headBlockNumber), queryEndBlock)
|
|
1515
1576
|
}
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// Each partition's existing in-flight itemsTarget, summed once and reused
|
|
1580
|
+
// both for chainReserved (the call-wide fresh-budget ceiling below) and to
|
|
1581
|
+
// seed each partition's water-fill footprint — so a partition already
|
|
1582
|
+
// holding in-flight queries is counted toward its even share and doesn't
|
|
1583
|
+
// get topped up past the others.
|
|
1584
|
+
let existingReservedByPartition = Dict.make()
|
|
1585
|
+
let chainReserved = ref(0.)
|
|
1586
|
+
for idx in 0 to partitionsCount - 1 {
|
|
1587
|
+
let partitionId = optimizedPartitions.idsInAscOrder->Array.getUnsafe(idx)
|
|
1588
|
+
let p = optimizedPartitions.entities->Dict.getUnsafe(partitionId)
|
|
1589
|
+
let cost = ref(0.)
|
|
1590
|
+
for pqIdx in 0 to p.mutPendingQueries->Array.length - 1 {
|
|
1591
|
+
cost :=
|
|
1592
|
+
cost.contents +. (p.mutPendingQueries->Array.getUnsafe(pqIdx)).itemsTarget->Int.toFloat
|
|
1593
|
+
}
|
|
1594
|
+
existingReservedByPartition->Dict.set(partitionId, cost.contents)
|
|
1595
|
+
chainReserved := chainReserved.contents +. cost.contents
|
|
1596
|
+
}
|
|
1516
1597
|
|
|
1598
|
+
// Phase A: gap-fill. Walk each partition's pending queries once,
|
|
1599
|
+
// unconditionally filling any hole (e.g. from an out-of-order partial
|
|
1600
|
+
// chunk response) — uncapped, same as before this tick's water-fill was
|
|
1601
|
+
// introduced. This also determines each partition's post-gap cursor and
|
|
1602
|
+
// whether it's blocked on an unresolved single-shot query.
|
|
1603
|
+
let fillStates = []
|
|
1604
|
+
let gapFillCost = ref(0.)
|
|
1605
|
+
for idx in 0 to partitionsCount - 1 {
|
|
1606
|
+
let partitionId = optimizedPartitions.idsInAscOrder->Array.getUnsafe(idx)
|
|
1607
|
+
let p = optimizedPartitions.entities->Dict.getUnsafe(partitionId)
|
|
1608
|
+
let bucket = queriesByPartitionIndex->Array.getUnsafe(idx)
|
|
1609
|
+
let pendingCount = p.mutPendingQueries->Array.length
|
|
1610
|
+
let queryEndBlock = computeQueryEndBlock(p)
|
|
1517
1611
|
let maybeChunkRange = getMinHistoryRange(p)
|
|
1518
1612
|
|
|
1519
|
-
// Walk pending queries to find open ranges and create queries for each
|
|
1520
1613
|
let cursor = ref(p.latestFetchedBlock.blockNumber + 1)
|
|
1521
1614
|
let canContinue = ref(true)
|
|
1615
|
+
let chunksUsedThisCall = ref(0)
|
|
1522
1616
|
let pqIdx = ref(0)
|
|
1523
|
-
while pqIdx.contents <
|
|
1617
|
+
while pqIdx.contents < pendingCount && canContinue.contents {
|
|
1524
1618
|
let pq = p.mutPendingQueries->Array.getUnsafe(pqIdx.contents)
|
|
1525
1619
|
|
|
1526
|
-
// Gap before this pending query → create queries for the gap range
|
|
1527
1620
|
if pq.fromBlock > cursor.contents {
|
|
1528
|
-
|
|
1529
|
-
|
|
1621
|
+
let beforeLen = bucket->Array.length
|
|
1622
|
+
let cost = pushGapFillQueries(
|
|
1623
|
+
bucket,
|
|
1530
1624
|
~partitionId,
|
|
1531
1625
|
~rangeFromBlock=cursor.contents,
|
|
1532
1626
|
~rangeEndBlock=Utils.Math.minOptInt(Some(pq.fromBlock - 1), queryEndBlock),
|
|
1533
1627
|
~knownHeight,
|
|
1628
|
+
~chainTargetBlock,
|
|
1534
1629
|
~maybeChunkRange,
|
|
1535
|
-
~maxChunks=maxPendingChunksPerPartition -
|
|
1536
|
-
pendingCount -
|
|
1537
|
-
(queries->Array.length -
|
|
1538
|
-
partitionQueriesStart),
|
|
1630
|
+
~maxChunks=maxPendingChunksPerPartition - pendingCount - chunksUsedThisCall.contents,
|
|
1539
1631
|
~partition=p,
|
|
1632
|
+
~partitionBudget=chainTargetItems /. partitionsCount->Int.toFloat,
|
|
1540
1633
|
~selection=p.selection,
|
|
1541
1634
|
~addressesByContractName=p.addressesByContractName,
|
|
1542
|
-
~partitionsCount,
|
|
1543
1635
|
)
|
|
1636
|
+
chunksUsedThisCall := chunksUsedThisCall.contents + (bucket->Array.length - beforeLen)
|
|
1637
|
+
gapFillCost := gapFillCost.contents +. cost
|
|
1544
1638
|
}
|
|
1545
1639
|
switch pq {
|
|
1546
1640
|
| {isChunk: true, toBlock: Some(toBlock), fetchedBlock: Some({blockNumber})}
|
|
@@ -1552,29 +1646,168 @@ let getNextQuery = (
|
|
|
1552
1646
|
pqIdx := pqIdx.contents + 1
|
|
1553
1647
|
}
|
|
1554
1648
|
|
|
1555
|
-
// Tail range after all pending queries
|
|
1556
1649
|
if canContinue.contents {
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
~selection=p.selection,
|
|
1570
|
-
~addressesByContractName=p.addressesByContractName,
|
|
1571
|
-
~partitionsCount,
|
|
1572
|
-
)
|
|
1650
|
+
fillStates
|
|
1651
|
+
->Array.push({
|
|
1652
|
+
partitionId,
|
|
1653
|
+
p,
|
|
1654
|
+
cursor: cursor.contents,
|
|
1655
|
+
chunksUsedThisCall: chunksUsedThisCall.contents,
|
|
1656
|
+
pendingCount,
|
|
1657
|
+
queryEndBlock,
|
|
1658
|
+
maybeChunkRange,
|
|
1659
|
+
bucket,
|
|
1660
|
+
})
|
|
1661
|
+
->ignore
|
|
1573
1662
|
}
|
|
1663
|
+
}
|
|
1574
1664
|
|
|
1575
|
-
|
|
1665
|
+
// Fresh budget for this call — what's left of chainTargetItems after
|
|
1666
|
+
// existing in-flight queries and this tick's gap-fill. The water-fill
|
|
1667
|
+
// rounds below hand it out; a partition already holding a large share
|
|
1668
|
+
// (seeded below) gets proportionally less so totals stay even.
|
|
1669
|
+
let rangeItemsTarget = Pervasives.max(
|
|
1670
|
+
0.,
|
|
1671
|
+
chainTargetItems -. chainReserved.contents -. gapFillCost.contents,
|
|
1672
|
+
)
|
|
1673
|
+
|
|
1674
|
+
// Each in-range partition's running footprint: existing in-flight plus any
|
|
1675
|
+
// gap-fill just pushed into its bucket. The water-fill levels every
|
|
1676
|
+
// partition up toward a shared line, so this seed is what a partition
|
|
1677
|
+
// starts the fill already holding.
|
|
1678
|
+
let reservedByPartition = Dict.make()
|
|
1679
|
+
fillStates->Array.forEach(fs => {
|
|
1680
|
+
let cost = ref(existingReservedByPartition->Dict.getUnsafe(fs.partitionId))
|
|
1681
|
+
for i in 0 to fs.bucket->Array.length - 1 {
|
|
1682
|
+
cost := cost.contents +. (fs.bucket->Array.getUnsafe(i)).itemsTarget->Int.toFloat
|
|
1683
|
+
}
|
|
1684
|
+
reservedByPartition->Dict.set(fs.partitionId, cost.contents)
|
|
1685
|
+
})
|
|
1686
|
+
|
|
1687
|
+
let isInRange = (fs: waterFillState) =>
|
|
1688
|
+
fs.cursor <= chainTargetBlock &&
|
|
1689
|
+
switch fs.queryEndBlock {
|
|
1690
|
+
| Some(eb) => fs.cursor <= eb
|
|
1691
|
+
| None => true
|
|
1692
|
+
} &&
|
|
1693
|
+
fs.pendingCount + fs.chunksUsedThisCall < maxPendingChunksPerPartition
|
|
1694
|
+
|
|
1695
|
+
let inRangeStates = fillStates->Array.filter(isInRange)
|
|
1696
|
+
|
|
1697
|
+
// Emits this round's queries for one partition, given its water-fill
|
|
1698
|
+
// allotment. Mutates fs.cursor/chunksUsedThisCall and returns the
|
|
1699
|
+
// itemsTarget consumed.
|
|
1700
|
+
//
|
|
1701
|
+
// Chunks require a POSITIVE trusted density: density 0 prices every chunk
|
|
1702
|
+
// at ~nothing, letting a partition flood its full chunk pipeline with
|
|
1703
|
+
// hard-bounded queries that crawl 1.8× per two responses — an open-ended
|
|
1704
|
+
// probe instead gets the server's full scan range in one response.
|
|
1705
|
+
let emitQueries = (fs: waterFillState, ~budget: float) => {
|
|
1706
|
+
let p = fs.p
|
|
1707
|
+
let maxBlock = switch fs.queryEndBlock {
|
|
1708
|
+
| Some(eb) => eb
|
|
1709
|
+
| None => chainTargetBlock
|
|
1710
|
+
}
|
|
1711
|
+
switch (fs.maybeChunkRange, p->getTrustedDensity) {
|
|
1712
|
+
| (Some(minHistoryRange), Some(density)) if density > 0. =>
|
|
1713
|
+
// Chunking active: strict chunks with a hard endBlock, uncapped real
|
|
1714
|
+
// density — at least one full chunk this round even if budget falls
|
|
1715
|
+
// short.
|
|
1716
|
+
let chunkSize = Js.Math.ceil_int(minHistoryRange->Int.toFloat *. 1.8)
|
|
1717
|
+
let chunkCost = density *. chunkSize->Int.toFloat
|
|
1718
|
+
let maxChunksRemaining =
|
|
1719
|
+
maxPendingChunksPerPartition - fs.pendingCount - fs.chunksUsedThisCall
|
|
1720
|
+
let affordable = Math.floor(budget /. chunkCost)->Float.toInt
|
|
1721
|
+
let numChunks = Pervasives.max(1, Pervasives.min(affordable, maxChunksRemaining))
|
|
1722
|
+
let consumed = ref(0.)
|
|
1723
|
+
let created = ref(0)
|
|
1724
|
+
let chunkFromBlock = ref(fs.cursor)
|
|
1725
|
+
while created.contents < numChunks && chunkFromBlock.contents <= maxBlock {
|
|
1726
|
+
let chunkToBlock = Pervasives.min(chunkFromBlock.contents + chunkSize - 1, maxBlock)
|
|
1727
|
+
let itemsTarget = Pervasives.max(
|
|
1728
|
+
1,
|
|
1729
|
+
(density *. (chunkToBlock - chunkFromBlock.contents + 1)->Int.toFloat)
|
|
1730
|
+
->Math.ceil
|
|
1731
|
+
->Float.toInt,
|
|
1732
|
+
)
|
|
1733
|
+
fs.bucket->Array.push({
|
|
1734
|
+
partitionId: fs.partitionId,
|
|
1735
|
+
fromBlock: chunkFromBlock.contents,
|
|
1736
|
+
toBlock: Some(chunkToBlock),
|
|
1737
|
+
isChunk: true,
|
|
1738
|
+
selection: p.selection,
|
|
1739
|
+
itemsTarget,
|
|
1740
|
+
addressesByContractName: p.addressesByContractName,
|
|
1741
|
+
})
|
|
1742
|
+
consumed := consumed.contents +. itemsTarget->Int.toFloat
|
|
1743
|
+
chunkFromBlock := chunkToBlock + 1
|
|
1744
|
+
created := created.contents + 1
|
|
1745
|
+
}
|
|
1746
|
+
fs.cursor = chunkFromBlock.contents
|
|
1747
|
+
fs.chunksUsedThisCall = fs.chunksUsedThisCall + created.contents
|
|
1748
|
+
consumed.contents
|
|
1749
|
+
| _ =>
|
|
1750
|
+
let itemsTarget = Pervasives.max(1, Math.round(budget)->Float.toInt)
|
|
1751
|
+
fs.bucket->Array.push({
|
|
1752
|
+
partitionId: fs.partitionId,
|
|
1753
|
+
fromBlock: fs.cursor,
|
|
1754
|
+
toBlock: fs.queryEndBlock,
|
|
1755
|
+
isChunk: false,
|
|
1756
|
+
selection: p.selection,
|
|
1757
|
+
itemsTarget,
|
|
1758
|
+
addressesByContractName: p.addressesByContractName,
|
|
1759
|
+
})
|
|
1760
|
+
fs.cursor = maxBlock + 1
|
|
1761
|
+
fs.chunksUsedThisCall = fs.chunksUsedThisCall + 1
|
|
1762
|
+
itemsTarget->Int.toFloat
|
|
1763
|
+
}
|
|
1576
1764
|
}
|
|
1577
1765
|
|
|
1766
|
+
// Water-fill. Range membership is fixed by chainTargetBlock/queryEndBlock,
|
|
1767
|
+
// so the in-range partitions are filtered once up front; each round pours
|
|
1768
|
+
// the remaining fresh budget at the waterLevel of the still-not-filled
|
|
1769
|
+
// partitions' footprints and keeps only those still in range for the next
|
|
1770
|
+
// round. Allotments sum to exactly the poured budget, so the outcome is
|
|
1771
|
+
// order-independent and never exceeds it — the only overshoot left is the
|
|
1772
|
+
// min-one-chunk quantization in emitQueries, bounded by one chunk per
|
|
1773
|
+
// partition per round.
|
|
1774
|
+
//
|
|
1775
|
+
// No explicit round cap: a partition survives a round only by advancing
|
|
1776
|
+
// chunksUsedThisCall (capped at maxPendingChunksPerPartition) or by
|
|
1777
|
+
// consuming fresh budget (every emit consumes at least 1), so the
|
|
1778
|
+
// not-filled set drains on its own and the loop also stops once the whole
|
|
1779
|
+
// budget is poured.
|
|
1780
|
+
let notFilledPartitions = ref(inRangeStates)
|
|
1781
|
+
let remainingBudget = ref(rangeItemsTarget)
|
|
1782
|
+
while notFilledPartitions.contents->Array.length > 0 && remainingBudget.contents > 0. {
|
|
1783
|
+
let level = waterLevel(
|
|
1784
|
+
~budget=remainingBudget.contents,
|
|
1785
|
+
~footprints=notFilledPartitions.contents->Array.map(fs =>
|
|
1786
|
+
reservedByPartition->Dict.getUnsafe(fs.partitionId)
|
|
1787
|
+
),
|
|
1788
|
+
)
|
|
1789
|
+
let next = []
|
|
1790
|
+
notFilledPartitions.contents->Array.forEach(fs => {
|
|
1791
|
+
let reserved = reservedByPartition->Dict.getUnsafe(fs.partitionId)
|
|
1792
|
+
let budget = level -. reserved
|
|
1793
|
+
if budget > 0. {
|
|
1794
|
+
let consumed = emitQueries(fs, ~budget)
|
|
1795
|
+
reservedByPartition->Dict.set(fs.partitionId, reserved +. consumed)
|
|
1796
|
+
remainingBudget := remainingBudget.contents -. consumed
|
|
1797
|
+
if fs->isInRange {
|
|
1798
|
+
next->Array.push(fs)->ignore
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
})
|
|
1802
|
+
notFilledPartitions := next
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
// Each partition pushed only into its own bucket (indexed by its
|
|
1806
|
+
// idsInAscOrder position), so flattening reproduces idsInAscOrder order
|
|
1807
|
+
// directly — no sort needed even though water-fill rounds interleave
|
|
1808
|
+
// across partitions.
|
|
1809
|
+
let queries = queriesByPartitionIndex->Array.flat
|
|
1810
|
+
|
|
1578
1811
|
if queries->Utils.Array.isEmpty {
|
|
1579
1812
|
if shouldWaitForNewBlock.contents {
|
|
1580
1813
|
WaitingForNewBlock
|
|
@@ -1623,7 +1856,7 @@ Instantiates a fetch state with partitions for initial addresses
|
|
|
1623
1856
|
let make = (
|
|
1624
1857
|
~startBlock,
|
|
1625
1858
|
~endBlock,
|
|
1626
|
-
~
|
|
1859
|
+
~onEventRegistrations: array<Internal.onEventRegistration>,
|
|
1627
1860
|
~contractConfigs: dict<IndexingAddresses.contractConfig>,
|
|
1628
1861
|
~addresses: array<Internal.indexingAddress>,
|
|
1629
1862
|
~maxAddrInPartition,
|
|
@@ -1631,7 +1864,7 @@ let make = (
|
|
|
1631
1864
|
~maxOnBlockBufferSize,
|
|
1632
1865
|
~knownHeight,
|
|
1633
1866
|
~progressBlockNumber=startBlock - 1,
|
|
1634
|
-
~
|
|
1867
|
+
~onBlockRegistrations=[],
|
|
1635
1868
|
~blockLag=0,
|
|
1636
1869
|
~firstEventBlock=None,
|
|
1637
1870
|
): t => {
|
|
@@ -1641,15 +1874,15 @@ let make = (
|
|
|
1641
1874
|
}
|
|
1642
1875
|
|
|
1643
1876
|
let notDependingOnAddresses = []
|
|
1644
|
-
let
|
|
1877
|
+
let normalRegistrations = []
|
|
1645
1878
|
let contractNamesWithNormalEvents = Utils.Set.make()
|
|
1646
1879
|
|
|
1647
|
-
|
|
1648
|
-
if
|
|
1649
|
-
|
|
1650
|
-
contractNamesWithNormalEvents->Utils.Set.add(
|
|
1880
|
+
onEventRegistrations->Array.forEach(reg => {
|
|
1881
|
+
if reg.dependsOnAddresses {
|
|
1882
|
+
normalRegistrations->Array.push(reg)
|
|
1883
|
+
contractNamesWithNormalEvents->Utils.Set.add(reg.eventConfig.contractName)->ignore
|
|
1651
1884
|
} else {
|
|
1652
|
-
notDependingOnAddresses->Array.push(
|
|
1885
|
+
notDependingOnAddresses->Array.push(reg)
|
|
1653
1886
|
}
|
|
1654
1887
|
})
|
|
1655
1888
|
|
|
@@ -1661,7 +1894,7 @@ let make = (
|
|
|
1661
1894
|
latestFetchedBlock,
|
|
1662
1895
|
selection: {
|
|
1663
1896
|
dependsOnAddresses: false,
|
|
1664
|
-
|
|
1897
|
+
onEventRegistrations: notDependingOnAddresses,
|
|
1665
1898
|
},
|
|
1666
1899
|
addressesByContractName: Dict.make(),
|
|
1667
1900
|
mergeBlock: None,
|
|
@@ -1676,7 +1909,7 @@ let make = (
|
|
|
1676
1909
|
|
|
1677
1910
|
let normalSelection = {
|
|
1678
1911
|
dependsOnAddresses: true,
|
|
1679
|
-
|
|
1912
|
+
onEventRegistrations: normalRegistrations,
|
|
1680
1913
|
}
|
|
1681
1914
|
|
|
1682
1915
|
let registeringContractsByContract: dict<dict<indexingAddress>> = Dict.make()
|
|
@@ -1712,12 +1945,15 @@ let make = (
|
|
|
1712
1945
|
~progressBlockNumber,
|
|
1713
1946
|
)
|
|
1714
1947
|
|
|
1715
|
-
if
|
|
1948
|
+
if (
|
|
1949
|
+
optimizedPartitions->OptimizedPartitions.count === 0 &&
|
|
1950
|
+
onBlockRegistrations->Utils.Array.isEmpty
|
|
1951
|
+
) {
|
|
1716
1952
|
JsError.throwWithMessage(
|
|
1717
1953
|
`Invalid configuration: Nothing to fetch on chain ${chainId->Int.toString}. ` ++
|
|
1718
1954
|
`addresses=${addresses->Array.length->Int.toString}, ` ++
|
|
1719
|
-
`
|
|
1720
|
-
`
|
|
1955
|
+
`onEventRegistrations=${onEventRegistrations->Array.length->Int.toString}, ` ++
|
|
1956
|
+
`normalRegistrations=${normalRegistrations
|
|
1721
1957
|
->Array.length
|
|
1722
1958
|
->Int.toString}. ` ++ `Make sure that you provided at least one contract address to index, or have events with Wildcard mode enabled, or have onBlock handlers.`,
|
|
1723
1959
|
)
|
|
@@ -1728,14 +1964,14 @@ let make = (
|
|
|
1728
1964
|
// fetching, so without seeding the buffer here getNextQuery would return
|
|
1729
1965
|
// NothingToQuery and the indexer would get stuck.
|
|
1730
1966
|
let buffer = []
|
|
1731
|
-
let latestOnBlockBlockNumber = if knownHeight > 0 &&
|
|
1967
|
+
let latestOnBlockBlockNumber = if knownHeight > 0 && onBlockRegistrations->Utils.Array.notEmpty {
|
|
1732
1968
|
let maxBlockNumber = switch optimizedPartitions->OptimizedPartitions.getLatestFullyFetchedBlock {
|
|
1733
1969
|
| None => knownHeight
|
|
1734
1970
|
| Some(latestFullyFetchedBlock) => latestFullyFetchedBlock.blockNumber
|
|
1735
1971
|
}
|
|
1736
1972
|
appendOnBlockItems(
|
|
1737
1973
|
~mutItems=buffer,
|
|
1738
|
-
~
|
|
1974
|
+
~onBlockRegistrations,
|
|
1739
1975
|
~indexerStartBlock=startBlock,
|
|
1740
1976
|
~fromBlock=progressBlockNumber,
|
|
1741
1977
|
~maxBlockNumber,
|
|
@@ -1754,7 +1990,7 @@ let make = (
|
|
|
1754
1990
|
latestOnBlockBlockNumber,
|
|
1755
1991
|
normalSelection,
|
|
1756
1992
|
blockLag,
|
|
1757
|
-
|
|
1993
|
+
onBlockRegistrations,
|
|
1758
1994
|
maxOnBlockBufferSize,
|
|
1759
1995
|
knownHeight,
|
|
1760
1996
|
buffer,
|
|
@@ -2004,17 +2240,6 @@ let getProgressPercentage = (fetchState: t) => {
|
|
|
2004
2240
|
}
|
|
2005
2241
|
}
|
|
2006
2242
|
|
|
2007
|
-
// Progress a specific block sits at along the chain, used to order queries from
|
|
2008
|
-
// different chains: a query starting further back (lower %) is fetched first.
|
|
2009
|
-
let getProgressPercentageAt = (fetchState: t, ~blockNumber) => {
|
|
2010
|
-
switch fetchState.firstEventBlock {
|
|
2011
|
-
| None => 0.
|
|
2012
|
-
| Some(firstEventBlock) =>
|
|
2013
|
-
let totalRange = fetchState.knownHeight - firstEventBlock
|
|
2014
|
-
totalRange <= 0 ? 0. : (blockNumber - firstEventBlock)->Int.toFloat /. totalRange->Int.toFloat
|
|
2015
|
-
}
|
|
2016
|
-
}
|
|
2017
|
-
|
|
2018
2243
|
let sortForBatch = {
|
|
2019
2244
|
let hasFullBatch = ({buffer} as fetchState: t, ~batchSizeTarget) => {
|
|
2020
2245
|
switch buffer->Array.get(batchSizeTarget - 1) {
|