envio 3.3.0-alpha.0 → 3.3.0-alpha.10
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/evm.schema.json +10 -0
- package/index.d.ts +45 -25
- package/package.json +7 -7
- package/src/Address.res +5 -2
- package/src/Address.res.mjs +3 -1
- package/src/Api.res +1 -1
- package/src/Api.res.mjs +1 -1
- package/src/Batch.res +0 -6
- package/src/Batch.res.mjs +1 -12
- package/src/BatchProcessing.res +7 -12
- package/src/BatchProcessing.res.mjs +8 -7
- package/src/Bin.res +3 -0
- package/src/Bin.res.mjs +4 -0
- package/src/ChainFetching.res +48 -16
- package/src/ChainFetching.res.mjs +36 -17
- package/src/ChainMetadata.res +1 -11
- package/src/ChainMetadata.res.mjs +1 -10
- package/src/ChainState.res +628 -149
- package/src/ChainState.res.mjs +440 -102
- package/src/ChainState.resi +74 -4
- package/src/Config.res +61 -33
- package/src/Config.res.mjs +50 -10
- package/src/ContractRegisterContext.res +2 -12
- package/src/ContractRegisterContext.res.mjs +3 -5
- package/src/Core.res +14 -3
- package/src/CrossChainState.res +142 -93
- package/src/CrossChainState.res.mjs +88 -60
- package/src/CrossChainState.resi +2 -10
- package/src/Ecosystem.res +5 -6
- package/src/Ecosystem.res.mjs +3 -3
- package/src/Env.res +0 -7
- package/src/Env.res.mjs +0 -6
- package/src/Envio.res +17 -11
- package/src/EnvioGlobal.res +53 -0
- package/src/EnvioGlobal.res.mjs +31 -0
- package/src/EventConfigBuilder.res +177 -88
- package/src/EventConfigBuilder.res.mjs +99 -42
- package/src/EventProcessing.res +74 -28
- package/src/EventProcessing.res.mjs +55 -29
- package/src/ExitOnCaughtUp.res +10 -2
- package/src/ExitOnCaughtUp.res.mjs +10 -1
- package/src/FetchState.res +558 -300
- package/src/FetchState.res.mjs +437 -423
- package/src/HandlerLoader.res +8 -104
- package/src/HandlerLoader.res.mjs +2 -81
- package/src/HandlerRegister.res +518 -144
- package/src/HandlerRegister.res.mjs +285 -133
- package/src/HandlerRegister.resi +24 -8
- package/src/IndexerLoop.res +2 -3
- package/src/IndexerLoop.res.mjs +1 -1
- package/src/IndexerState.res +10 -11
- package/src/IndexerState.res.mjs +14 -11
- package/src/IndexerState.resi +2 -5
- package/src/IndexingAddresses.res +108 -0
- package/src/IndexingAddresses.res.mjs +101 -0
- package/src/IndexingAddresses.resi +34 -0
- package/src/Internal.res +155 -46
- package/src/Internal.res.mjs +30 -2
- package/src/LoadLayer.res +5 -5
- package/src/LoadLayer.res.mjs +6 -6
- package/src/LogSelection.res +102 -165
- package/src/LogSelection.res.mjs +101 -116
- package/src/Main.res +71 -236
- package/src/Main.res.mjs +60 -145
- package/src/Metrics.res +74 -0
- package/src/Metrics.res.mjs +75 -0
- package/src/PgStorage.res +4 -4
- package/src/PgStorage.res.mjs +5 -5
- package/src/Prometheus.res +10 -74
- package/src/Prometheus.res.mjs +124 -186
- package/src/PruneStaleHistory.res +2 -2
- package/src/PruneStaleHistory.res.mjs +3 -3
- package/src/RawEvent.res +7 -2
- package/src/RawEvent.res.mjs +4 -4
- package/src/Rollback.res +3 -3
- package/src/Rollback.res.mjs +4 -4
- package/src/RollbackCommit.res +4 -1
- package/src/RollbackCommit.res.mjs +3 -2
- package/src/SimulateDeadInputTracker.res +85 -0
- package/src/SimulateDeadInputTracker.res.mjs +73 -0
- package/src/SimulateDeadInputTracker.resi +12 -0
- package/src/SimulateItems.res +91 -23
- package/src/SimulateItems.res.mjs +55 -32
- package/src/TestIndexer.res +54 -40
- package/src/TestIndexer.res.mjs +37 -30
- package/src/bindings/Performance.res +7 -0
- package/src/bindings/Performance.res.mjs +21 -0
- package/src/bindings/Performance.resi +7 -0
- package/src/bindings/Viem.res +0 -41
- package/src/bindings/Viem.res.mjs +1 -43
- package/src/sources/BlockStore.res +46 -0
- package/src/sources/BlockStore.res.mjs +24 -0
- package/src/sources/EventRouter.res +36 -33
- package/src/sources/EventRouter.res.mjs +13 -14
- package/src/sources/Evm.res +83 -72
- package/src/sources/Evm.res.mjs +53 -69
- package/src/sources/EvmChain.res +18 -20
- package/src/sources/EvmChain.res.mjs +15 -15
- package/src/sources/EvmRpcClient.res +74 -5
- package/src/sources/EvmRpcClient.res.mjs +13 -4
- package/src/sources/FieldMask.res +39 -0
- package/src/sources/FieldMask.res.mjs +42 -0
- package/src/sources/Fuel.res +9 -6
- package/src/sources/Fuel.res.mjs +5 -10
- package/src/sources/HyperFuelSource.res +46 -50
- package/src/sources/HyperFuelSource.res.mjs +64 -54
- package/src/sources/HyperSync.res +39 -7
- package/src/sources/HyperSync.res.mjs +57 -37
- package/src/sources/HyperSync.resi +12 -2
- package/src/sources/HyperSyncClient.res +22 -111
- package/src/sources/HyperSyncClient.res.mjs +2 -30
- package/src/sources/HyperSyncHeightStream.res +1 -8
- package/src/sources/HyperSyncHeightStream.res.mjs +0 -2
- package/src/sources/HyperSyncSource.res +94 -107
- package/src/sources/HyperSyncSource.res.mjs +82 -86
- package/src/sources/Rpc.res +15 -47
- package/src/sources/Rpc.res.mjs +25 -56
- package/src/sources/RpcSource.res +320 -467
- package/src/sources/RpcSource.res.mjs +268 -385
- package/src/sources/RpcWebSocketHeightStream.res +0 -5
- package/src/sources/RpcWebSocketHeightStream.res.mjs +0 -2
- package/src/sources/SimulateSource.res +10 -4
- package/src/sources/SimulateSource.res.mjs +16 -6
- package/src/sources/Source.res +34 -6
- package/src/sources/SourceManager.res +86 -27
- package/src/sources/SourceManager.res.mjs +84 -47
- package/src/sources/SourceManager.resi +12 -3
- package/src/sources/Svm.res +31 -16
- package/src/sources/Svm.res.mjs +36 -13
- package/src/sources/SvmHyperSyncClient.res +5 -32
- package/src/sources/SvmHyperSyncSource.res +159 -134
- package/src/sources/SvmHyperSyncSource.res.mjs +164 -129
- package/src/sources/TransactionStore.res +49 -0
- package/src/sources/TransactionStore.res.mjs +30 -0
- package/src/tui/Tui.res +13 -16
- package/src/tui/Tui.res.mjs +12 -14
- package/svm.schema.json +49 -37
- package/src/bindings/Hrtime.res +0 -58
- package/src/bindings/Hrtime.res.mjs +0 -90
- package/src/bindings/Hrtime.resi +0 -30
package/src/FetchState.res
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
type contractConfig = {startBlock: option<int>}
|
|
2
|
-
|
|
3
1
|
type indexingAddress = Internal.indexingContract
|
|
4
2
|
|
|
5
|
-
let deriveEffectiveStartBlock = (~registrationBlock: int, ~contractStartBlock: option<int>) => {
|
|
6
|
-
Pervasives.max(Pervasives.max(registrationBlock, 0), contractStartBlock->Option.getOr(0))
|
|
7
|
-
}
|
|
8
|
-
|
|
9
3
|
type blockNumberAndTimestamp = {
|
|
10
4
|
blockNumber: int,
|
|
11
5
|
blockTimestamp: int,
|
|
@@ -13,12 +7,21 @@ type blockNumberAndTimestamp = {
|
|
|
13
7
|
|
|
14
8
|
type blockNumberAndLogIndex = {blockNumber: int, logIndex: int}
|
|
15
9
|
|
|
16
|
-
type selection = {
|
|
10
|
+
type selection = {
|
|
11
|
+
onEventRegistrations: array<Internal.onEventRegistration>,
|
|
12
|
+
dependsOnAddresses: bool,
|
|
13
|
+
}
|
|
17
14
|
|
|
18
15
|
type pendingQuery = {
|
|
19
16
|
fromBlock: int,
|
|
20
17
|
toBlock: option<int>,
|
|
21
18
|
isChunk: bool,
|
|
19
|
+
// Items this in-flight query is targeting (server maxNumLogs-style cap).
|
|
20
|
+
itemsTarget: int,
|
|
21
|
+
// Estimated items this in-flight query will actually return (no headroom),
|
|
22
|
+
// carried from the query so the shared buffer budget can account for what's
|
|
23
|
+
// already being fetched without the cap's safety margin inflating it.
|
|
24
|
+
itemsEst: int,
|
|
22
25
|
// Stores latestFetchedBlock when query completes. Only needed to persist
|
|
23
26
|
// timestamp while earlier queries are still pending before updating
|
|
24
27
|
// the partition's latestFetchedBlock.
|
|
@@ -47,6 +50,10 @@ type partition = {
|
|
|
47
50
|
// Track last 3 successful query ranges for chunking heuristic (0 means no data)
|
|
48
51
|
prevQueryRange: int,
|
|
49
52
|
prevPrevQueryRange: int,
|
|
53
|
+
// Item count of the response that set prevQueryRange. Paired with it to derive
|
|
54
|
+
// the partition's event density (prevRangeSize / prevQueryRange) for sizing
|
|
55
|
+
// queries against the buffer budget.
|
|
56
|
+
prevRangeSize: int,
|
|
50
57
|
// Tracks the latestFetchedBlock.blockNumber of the most recent response
|
|
51
58
|
// that updated prevQueryRange. Prevents degradation of the chunking heuristic
|
|
52
59
|
// when parallel query responses arrive out of order.
|
|
@@ -58,9 +65,50 @@ type query = {
|
|
|
58
65
|
fromBlock: int,
|
|
59
66
|
toBlock: option<int>,
|
|
60
67
|
isChunk: bool,
|
|
68
|
+
// Items this query targets: the server-side maxNumLogs-style cap, sized
|
|
69
|
+
// with headroom (chunkItemsMultiplier) so a denser-than-expected range
|
|
70
|
+
// doesn't truncate the response.
|
|
71
|
+
itemsTarget: int,
|
|
72
|
+
// Expected items without headroom: density × the query's block range for a
|
|
73
|
+
// known-density partition, the query's budget share otherwise. This is the
|
|
74
|
+
// unit the chain's per-tick budget is reserved/consumed in — reserving the
|
|
75
|
+
// headroomed cap instead would throttle the pipeline by the safety margin.
|
|
76
|
+
itemsEst: int,
|
|
61
77
|
selection: selection,
|
|
62
78
|
addressesByContractName: dict<array<Address.t>>,
|
|
63
|
-
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Invert addressesByContractName into address→contractName for log-ownership
|
|
82
|
+
// routing. 1:1 today (each address belongs to one contract), so no key
|
|
83
|
+
// collisions. Memoized on the addressesByContractName object so a partition's
|
|
84
|
+
// many responses share one derivation and a large factory never rebuilds the
|
|
85
|
+
// whole index; sound because the dict is immutable after construction (every
|
|
86
|
+
// mutation produces a new dict).
|
|
87
|
+
let deriveContractNameByAddress: dict<array<Address.t>> => dict<
|
|
88
|
+
string,
|
|
89
|
+
> = Utils.WeakMap.memoize(addressesByContractName => {
|
|
90
|
+
let result = Dict.make()
|
|
91
|
+
addressesByContractName->Utils.Dict.forEachWithKey((addresses, contractName) => {
|
|
92
|
+
for i in 0 to addresses->Array.length - 1 {
|
|
93
|
+
result->Dict.set(addresses->Array.getUnsafe(i)->Address.toString, contractName)
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
result
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
// itemsTarget for a query over [fromBlock, toBlock] at the given event density
|
|
100
|
+
// (items/block). toBlock None is the open-ended tail, capped at
|
|
101
|
+
// chainTargetBlock — the soft per-tick horizon the owning chain wants to reach
|
|
102
|
+
// (see getNextQuery).
|
|
103
|
+
let densityItemsTarget = (~density, ~fromBlock, ~toBlock, ~chainTargetBlock) => {
|
|
104
|
+
// Floor at 1: the reservation must equal the server-side cap SourceManager
|
|
105
|
+
// sends, and a 0 cap would ask the backend for nothing.
|
|
106
|
+
Pervasives.max(
|
|
107
|
+
1,
|
|
108
|
+
((toBlock->Option.getOr(chainTargetBlock) - fromBlock + 1)->Int.toFloat *. density)
|
|
109
|
+
->Math.ceil
|
|
110
|
+
->Float.toInt,
|
|
111
|
+
)
|
|
64
112
|
}
|
|
65
113
|
|
|
66
114
|
// Calculate the chunk range from history using min-of-last-3-ranges heuristic
|
|
@@ -71,6 +119,15 @@ let getMinHistoryRange = (p: partition) => {
|
|
|
71
119
|
}
|
|
72
120
|
}
|
|
73
121
|
|
|
122
|
+
// Density (items/block) from the last response, trusted only after two
|
|
123
|
+
// responses — a single sample is too noisy to size the next query by.
|
|
124
|
+
let getTrustedDensity = (p: partition) => {
|
|
125
|
+
switch (p.prevQueryRange, p.prevPrevQueryRange) {
|
|
126
|
+
| (0, _) | (_, 0) => None
|
|
127
|
+
| (prevQueryRange, _) => Some(p.prevRangeSize->Int.toFloat /. prevQueryRange->Int.toFloat)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
74
131
|
let getMinQueryRange = (partitions: array<partition>) => {
|
|
75
132
|
let min = ref(0)
|
|
76
133
|
for i in 0 to partitions->Array.length - 1 {
|
|
@@ -150,6 +207,12 @@ module OptimizedPartitions = {
|
|
|
150
207
|
let newId = nextPartitionIndexRef.contents->Int.toString
|
|
151
208
|
nextPartitionIndexRef := nextPartitionIndexRef.contents + 1
|
|
152
209
|
let minRange = getMinQueryRange([p1, p2])
|
|
210
|
+
// The merged partition indexes both parents' addresses, so its expected
|
|
211
|
+
// event rate is the sum of their densities. Parents without a trusted
|
|
212
|
+
// density contribute 0; if none has one, prevRangeSize stays 0 and the
|
|
213
|
+
// partition probes for a fresh signal instead of chunking.
|
|
214
|
+
let inheritedDensity =
|
|
215
|
+
p1->getTrustedDensity->Option.getOr(0.) +. p2->getTrustedDensity->Option.getOr(0.)
|
|
153
216
|
{
|
|
154
217
|
id: newId,
|
|
155
218
|
dynamicContract: Some(contractName),
|
|
@@ -160,6 +223,7 @@ module OptimizedPartitions = {
|
|
|
160
223
|
mutPendingQueries: [],
|
|
161
224
|
prevQueryRange: minRange,
|
|
162
225
|
prevPrevQueryRange: minRange,
|
|
226
|
+
prevRangeSize: (inheritedDensity *. minRange->Int.toFloat)->Math.ceil->Float.toInt,
|
|
163
227
|
latestBlockRangeUpdateBlock: 0,
|
|
164
228
|
}
|
|
165
229
|
}
|
|
@@ -397,6 +461,7 @@ module OptimizedPartitions = {
|
|
|
397
461
|
optimizedPartitions: t,
|
|
398
462
|
~query,
|
|
399
463
|
~knownHeight,
|
|
464
|
+
~itemsCount,
|
|
400
465
|
~latestFetchedBlock: blockNumberAndTimestamp,
|
|
401
466
|
) => {
|
|
402
467
|
let p = optimizedPartitions->getOrThrow(~partitionId=query.partitionId)
|
|
@@ -416,18 +481,25 @@ module OptimizedPartitions = {
|
|
|
416
481
|
switch query.toBlock {
|
|
417
482
|
| None => latestFetchedBlock.blockNumber < knownHeight - 10 // Don't update block range when very close to the head
|
|
418
483
|
| Some(queryToBlock) =>
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
484
|
+
if latestFetchedBlock.blockNumber < queryToBlock {
|
|
485
|
+
// Partial response is direct capacity evidence — unless it was
|
|
486
|
+
// truncated by our own itemsTarget cap: that reflects the
|
|
487
|
+
// reservation we asked for, not what the server could return.
|
|
488
|
+
itemsCount < query.itemsTarget
|
|
489
|
+
} else {
|
|
490
|
+
// A full response updates only when the query's intended range
|
|
491
|
+
// covers at least the partition's current chunk range — meaning it
|
|
492
|
+
// was a capacity-based split chunk, not a small gap-fill whose
|
|
493
|
+
// toBlock is an artificial boundary.
|
|
424
494
|
switch getMinHistoryRange(p) {
|
|
425
495
|
| None => false // Chunking not active yet, don't update
|
|
426
496
|
| Some(minHistoryRange) => queryToBlock - query.fromBlock + 1 >= minHistoryRange
|
|
427
497
|
}
|
|
498
|
+
}
|
|
428
499
|
}
|
|
429
500
|
let updatedPrevQueryRange = shouldUpdateBlockRange ? blockRange : p.prevQueryRange
|
|
430
501
|
let updatedPrevPrevQueryRange = shouldUpdateBlockRange ? p.prevQueryRange : p.prevPrevQueryRange
|
|
502
|
+
let updatedPrevRangeSize = shouldUpdateBlockRange ? itemsCount : p.prevRangeSize
|
|
431
503
|
|
|
432
504
|
// Process fetched queries from front of queue for main partition
|
|
433
505
|
let updatedLatestFetchedBlock = consumeFetchedQueries(
|
|
@@ -449,6 +521,7 @@ module OptimizedPartitions = {
|
|
|
449
521
|
latestFetchedBlock: updatedLatestFetchedBlock,
|
|
450
522
|
prevQueryRange: updatedPrevQueryRange,
|
|
451
523
|
prevPrevQueryRange: updatedPrevPrevQueryRange,
|
|
524
|
+
prevRangeSize: updatedPrevRangeSize,
|
|
452
525
|
latestBlockRangeUpdateBlock: shouldUpdateBlockRange
|
|
453
526
|
? latestFetchedBlock.blockNumber
|
|
454
527
|
: p.latestBlockRangeUpdateBlock,
|
|
@@ -480,10 +553,8 @@ type t = {
|
|
|
480
553
|
startBlock: int,
|
|
481
554
|
endBlock: option<int>,
|
|
482
555
|
normalSelection: selection,
|
|
483
|
-
// By address
|
|
484
|
-
indexingAddresses: dict<indexingAddress>,
|
|
485
556
|
// By contract name
|
|
486
|
-
contractConfigs: dict<contractConfig>,
|
|
557
|
+
contractConfigs: dict<IndexingAddresses.contractConfig>,
|
|
487
558
|
// Not used for logic - only metadata
|
|
488
559
|
chainId: int,
|
|
489
560
|
// The block number of the latest block which was added to the queue
|
|
@@ -497,10 +568,10 @@ type t = {
|
|
|
497
568
|
// Buffer of items ordered from earliest to latest
|
|
498
569
|
buffer: array<Internal.item>,
|
|
499
570
|
// Caps how far ahead onBlock items are pre-generated (set to 2x the batch
|
|
500
|
-
// size).
|
|
501
|
-
// chain
|
|
571
|
+
// size). Event fetch depth is bounded separately, by CrossChainState's
|
|
572
|
+
// cross-chain admission against the indexer-wide buffer pool.
|
|
502
573
|
maxOnBlockBufferSize: int,
|
|
503
|
-
|
|
574
|
+
onBlockRegistrations: array<Internal.onBlockRegistration>,
|
|
504
575
|
knownHeight: int,
|
|
505
576
|
firstEventBlock: option<int>,
|
|
506
577
|
}
|
|
@@ -536,6 +607,25 @@ let bufferBlock = ({optimizedPartitions, latestOnBlockBlockNumber}: t) => {
|
|
|
536
607
|
}
|
|
537
608
|
}
|
|
538
609
|
|
|
610
|
+
// Number of buffered items at or below the ready frontier (processable now,
|
|
611
|
+
// i.e. not stuck behind a gap from a lagging partition or out-of-order chunk).
|
|
612
|
+
// The buffer is kept sorted, so binary-search the frontier in O(log n).
|
|
613
|
+
let bufferReadyCount = (fetchState: t) => {
|
|
614
|
+
let frontier = fetchState->bufferBlockNumber
|
|
615
|
+
let buffer = fetchState.buffer
|
|
616
|
+
let lo = ref(0)
|
|
617
|
+
let hi = ref(buffer->Array.length)
|
|
618
|
+
while lo.contents < hi.contents {
|
|
619
|
+
let mid = (lo.contents + hi.contents) / 2
|
|
620
|
+
if buffer->Array.getUnsafe(mid)->Internal.getItemBlockNumber <= frontier {
|
|
621
|
+
lo := mid + 1
|
|
622
|
+
} else {
|
|
623
|
+
hi := mid
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
lo.contents
|
|
627
|
+
}
|
|
628
|
+
|
|
539
629
|
/*
|
|
540
630
|
Comparitor for two events from the same chain. No need for chain id or timestamp
|
|
541
631
|
*/
|
|
@@ -551,15 +641,13 @@ let compareBufferItem = (a: Internal.item, b: Internal.item) => {
|
|
|
551
641
|
// Some big number which should be bigger than any log index
|
|
552
642
|
let blockItemLogIndex = 16777216
|
|
553
643
|
|
|
554
|
-
let numAddresses = fetchState => fetchState.indexingAddresses->Utils.Dict.size
|
|
555
|
-
|
|
556
644
|
// Appends Block items produced by the onBlock handlers for every block in
|
|
557
645
|
// (fromBlock, maxBlockNumber] into mutItems and returns the new
|
|
558
646
|
// latestOnBlockBlockNumber pointer. maxOnBlockBufferSize bounds how many items
|
|
559
647
|
// are generated at once to prevent OOM.
|
|
560
648
|
let appendOnBlockItems = (
|
|
561
649
|
~mutItems: array<Internal.item>,
|
|
562
|
-
~
|
|
650
|
+
~onBlockRegistrations: array<Internal.onBlockRegistration>,
|
|
563
651
|
~indexerStartBlock,
|
|
564
652
|
~fromBlock,
|
|
565
653
|
~maxBlockNumber,
|
|
@@ -579,27 +667,27 @@ let appendOnBlockItems = (
|
|
|
579
667
|
let blockNumber = latestOnBlockBlockNumber.contents + 1
|
|
580
668
|
latestOnBlockBlockNumber := blockNumber
|
|
581
669
|
|
|
582
|
-
for configIdx in 0 to
|
|
583
|
-
let
|
|
670
|
+
for configIdx in 0 to onBlockRegistrations->Array.length - 1 {
|
|
671
|
+
let onBlockRegistration = onBlockRegistrations->Array.getUnsafe(configIdx)
|
|
584
672
|
|
|
585
|
-
let handlerStartBlock = switch
|
|
673
|
+
let handlerStartBlock = switch onBlockRegistration.startBlock {
|
|
586
674
|
| Some(startBlock) => startBlock
|
|
587
675
|
| None => indexerStartBlock
|
|
588
676
|
}
|
|
589
677
|
|
|
590
678
|
if (
|
|
591
679
|
blockNumber >= handlerStartBlock &&
|
|
592
|
-
switch
|
|
680
|
+
switch onBlockRegistration.endBlock {
|
|
593
681
|
| Some(endBlock) => blockNumber <= endBlock
|
|
594
682
|
| None => true
|
|
595
683
|
} &&
|
|
596
|
-
(blockNumber - handlerStartBlock)->Pervasives.mod(
|
|
684
|
+
(blockNumber - handlerStartBlock)->Pervasives.mod(onBlockRegistration.interval) === 0
|
|
597
685
|
) {
|
|
598
686
|
mutItems->Array.push(
|
|
599
687
|
Block({
|
|
600
|
-
|
|
688
|
+
onBlockRegistration,
|
|
601
689
|
blockNumber,
|
|
602
|
-
logIndex: blockItemLogIndex +
|
|
690
|
+
logIndex: blockItemLogIndex + onBlockRegistration.index,
|
|
603
691
|
}),
|
|
604
692
|
)
|
|
605
693
|
newItemsCounter := newItemsCounter.contents + 1
|
|
@@ -617,16 +705,15 @@ Runs partition optimization when partitions change.
|
|
|
617
705
|
let updateInternal = (
|
|
618
706
|
fetchState: t,
|
|
619
707
|
~optimizedPartitions=fetchState.optimizedPartitions,
|
|
620
|
-
~indexingAddresses=fetchState.indexingAddresses,
|
|
621
708
|
~mutItems=?,
|
|
622
709
|
~blockLag=fetchState.blockLag,
|
|
623
710
|
~knownHeight=fetchState.knownHeight,
|
|
624
711
|
): t => {
|
|
625
712
|
let mutItemsRef = ref(mutItems)
|
|
626
713
|
|
|
627
|
-
let latestOnBlockBlockNumber = switch fetchState.
|
|
714
|
+
let latestOnBlockBlockNumber = switch fetchState.onBlockRegistrations {
|
|
628
715
|
| [] => knownHeight
|
|
629
|
-
|
|
|
716
|
+
| onBlockRegistrations => {
|
|
630
717
|
// Calculate the max block number we are going to create items for
|
|
631
718
|
// Use maxOnBlockBufferSize to get the last target item in the buffer
|
|
632
719
|
//
|
|
@@ -654,7 +741,7 @@ let updateInternal = (
|
|
|
654
741
|
|
|
655
742
|
appendOnBlockItems(
|
|
656
743
|
~mutItems,
|
|
657
|
-
~
|
|
744
|
+
~onBlockRegistrations,
|
|
658
745
|
~indexerStartBlock=fetchState.startBlock,
|
|
659
746
|
~fromBlock=fetchState.latestOnBlockBlockNumber,
|
|
660
747
|
~maxBlockNumber,
|
|
@@ -669,11 +756,10 @@ let updateInternal = (
|
|
|
669
756
|
contractConfigs: fetchState.contractConfigs,
|
|
670
757
|
normalSelection: fetchState.normalSelection,
|
|
671
758
|
chainId: fetchState.chainId,
|
|
672
|
-
|
|
759
|
+
onBlockRegistrations: fetchState.onBlockRegistrations,
|
|
673
760
|
maxOnBlockBufferSize: fetchState.maxOnBlockBufferSize,
|
|
674
761
|
optimizedPartitions,
|
|
675
762
|
latestOnBlockBlockNumber,
|
|
676
|
-
indexingAddresses,
|
|
677
763
|
blockLag,
|
|
678
764
|
knownHeight,
|
|
679
765
|
buffer: switch mutItemsRef.contents {
|
|
@@ -701,12 +787,6 @@ let updateInternal = (
|
|
|
701
787
|
~blockNumber=updatedFetchState->bufferBlockNumber,
|
|
702
788
|
~chainId=fetchState.chainId,
|
|
703
789
|
)
|
|
704
|
-
if indexingAddresses !== fetchState.indexingAddresses {
|
|
705
|
-
Prometheus.IndexingAddresses.set(
|
|
706
|
-
~addressesCount=updatedFetchState->numAddresses,
|
|
707
|
-
~chainId=fetchState.chainId,
|
|
708
|
-
)
|
|
709
|
-
}
|
|
710
790
|
|
|
711
791
|
updatedFetchState
|
|
712
792
|
}
|
|
@@ -838,6 +918,7 @@ OptimizedPartitions.t => {
|
|
|
838
918
|
mutPendingQueries: [],
|
|
839
919
|
prevQueryRange: 0,
|
|
840
920
|
prevPrevQueryRange: 0,
|
|
921
|
+
prevRangeSize: 0,
|
|
841
922
|
latestBlockRangeUpdateBlock: 0,
|
|
842
923
|
})
|
|
843
924
|
nextPartitionIndexRef := nextPartitionIndexRef.contents + 1
|
|
@@ -936,22 +1017,22 @@ OptimizedPartitions.t => {
|
|
|
936
1017
|
|
|
937
1018
|
let registerDynamicContracts = (
|
|
938
1019
|
fetchState: t,
|
|
1020
|
+
~indexingAddresses: IndexingAddresses.t,
|
|
939
1021
|
// These are raw items which might have dynamic contracts received from contractRegister call.
|
|
940
1022
|
// Might contain duplicates which we should filter out
|
|
941
1023
|
items: array<Internal.item>,
|
|
942
1024
|
) => {
|
|
943
|
-
if fetchState.normalSelection.
|
|
1025
|
+
if fetchState.normalSelection.onEventRegistrations->Utils.Array.isEmpty {
|
|
944
1026
|
// Can the normalSelection be empty?
|
|
945
1027
|
JsError.throwWithMessage(
|
|
946
1028
|
"Invalid configuration. No events to fetch for the dynamic contract registration.",
|
|
947
1029
|
)
|
|
948
1030
|
}
|
|
949
1031
|
|
|
950
|
-
let indexingAddresses = fetchState.indexingAddresses
|
|
951
1032
|
let registeringContractsByContract: dict<dict<indexingAddress>> = Dict.make()
|
|
952
1033
|
let earliestRegisteringEventBlockNumber = ref(%raw(`Infinity`))
|
|
953
1034
|
// Addresses registered for contracts without matching events. These are not
|
|
954
|
-
// added to partitions, but they are tracked on
|
|
1035
|
+
// added to partitions, but they are tracked on indexingAddresses
|
|
955
1036
|
// so that later conflicting registrations are detected, and are persisted
|
|
956
1037
|
// to envio_addresses so they can be picked up on restart with updated config.
|
|
957
1038
|
let noEventsAddresses: dict<indexingAddress> = Dict.make()
|
|
@@ -977,15 +1058,13 @@ let registerDynamicContracts = (
|
|
|
977
1058
|
address: dc.address,
|
|
978
1059
|
contractName: dc.contractName,
|
|
979
1060
|
registrationBlock: dc.registrationBlock,
|
|
980
|
-
effectiveStartBlock: deriveEffectiveStartBlock(
|
|
1061
|
+
effectiveStartBlock: IndexingAddresses.deriveEffectiveStartBlock(
|
|
981
1062
|
~registrationBlock=dc.registrationBlock,
|
|
982
1063
|
~contractStartBlock,
|
|
983
1064
|
),
|
|
984
1065
|
}
|
|
985
1066
|
// Prevent registering already indexing contracts
|
|
986
|
-
switch indexingAddresses->
|
|
987
|
-
dc.address->Address.toString,
|
|
988
|
-
) {
|
|
1067
|
+
switch indexingAddresses->IndexingAddresses.get(dc.address->Address.toString) {
|
|
989
1068
|
| Some(existingContract) =>
|
|
990
1069
|
// FIXME: Instead of filtering out duplicates,
|
|
991
1070
|
// we should check the block number first.
|
|
@@ -1040,7 +1119,7 @@ let registerDynamicContracts = (
|
|
|
1040
1119
|
address: dc.address,
|
|
1041
1120
|
contractName: dc.contractName,
|
|
1042
1121
|
registrationBlock: dc.registrationBlock,
|
|
1043
|
-
effectiveStartBlock: deriveEffectiveStartBlock(
|
|
1122
|
+
effectiveStartBlock: IndexingAddresses.deriveEffectiveStartBlock(
|
|
1044
1123
|
~registrationBlock=dc.registrationBlock,
|
|
1045
1124
|
~contractStartBlock=None,
|
|
1046
1125
|
),
|
|
@@ -1048,9 +1127,7 @@ let registerDynamicContracts = (
|
|
|
1048
1127
|
// Prevent duplicate logging/persistence when the same address is
|
|
1049
1128
|
// already tracked on fetchState, either from the db on startup or
|
|
1050
1129
|
// from an earlier registration in this batch.
|
|
1051
|
-
switch indexingAddresses->
|
|
1052
|
-
dc.address->Address.toString,
|
|
1053
|
-
) {
|
|
1130
|
+
switch indexingAddresses->IndexingAddresses.get(dc.address->Address.toString) {
|
|
1054
1131
|
| Some(existingContract) =>
|
|
1055
1132
|
if existingContract.contractName != dc.contractName {
|
|
1056
1133
|
fetchState->warnDifferentContractType(~existingContract, ~dc=dcAsIndexingAddress)
|
|
@@ -1104,12 +1181,10 @@ let registerDynamicContracts = (
|
|
|
1104
1181
|
// Only dcs for contracts without events. Track them on
|
|
1105
1182
|
// indexingAddresses so subsequent registrations see them, but don't touch
|
|
1106
1183
|
// partitions since there's nothing to fetch for them.
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
fetchState->updateInternal(~indexingAddresses=newIndexingContracts)
|
|
1184
|
+
indexingAddresses->IndexingAddresses.register(noEventsAddresses)
|
|
1185
|
+
fetchState
|
|
1110
1186
|
| (_, _) => {
|
|
1111
1187
|
let newPartitions = []
|
|
1112
|
-
let newIndexingAddresses = indexingAddresses->Utils.Dict.shallowCopy
|
|
1113
1188
|
let dynamicContractsRef = ref(fetchState.optimizedPartitions.dynamicContracts)
|
|
1114
1189
|
let mutExistingPartitions = fetchState.optimizedPartitions.entities->Dict.valuesToArray
|
|
1115
1190
|
|
|
@@ -1179,6 +1254,7 @@ let registerDynamicContracts = (
|
|
|
1179
1254
|
mutPendingQueries: p.mutPendingQueries,
|
|
1180
1255
|
prevQueryRange: p.prevQueryRange,
|
|
1181
1256
|
prevPrevQueryRange: p.prevPrevQueryRange,
|
|
1257
|
+
prevRangeSize: p.prevRangeSize,
|
|
1182
1258
|
latestBlockRangeUpdateBlock: p.latestBlockRangeUpdateBlock,
|
|
1183
1259
|
})
|
|
1184
1260
|
}
|
|
@@ -1190,10 +1266,10 @@ let registerDynamicContracts = (
|
|
|
1190
1266
|
}
|
|
1191
1267
|
|
|
1192
1268
|
let registeringContracts = registeringContractsByContract->Dict.getUnsafe(contractName)
|
|
1193
|
-
|
|
1269
|
+
indexingAddresses->IndexingAddresses.register(registeringContracts)
|
|
1194
1270
|
}
|
|
1195
1271
|
// Include no-events dcs so later batches detect conflicts against them.
|
|
1196
|
-
|
|
1272
|
+
indexingAddresses->IndexingAddresses.register(noEventsAddresses)
|
|
1197
1273
|
|
|
1198
1274
|
let optimizedPartitions = createPartitionsFromIndexingAddresses(
|
|
1199
1275
|
~registeringContractsByContract,
|
|
@@ -1206,7 +1282,7 @@ let registerDynamicContracts = (
|
|
|
1206
1282
|
~progressBlockNumber=0,
|
|
1207
1283
|
)
|
|
1208
1284
|
|
|
1209
|
-
fetchState->updateInternal(~optimizedPartitions
|
|
1285
|
+
fetchState->updateInternal(~optimizedPartitions)
|
|
1210
1286
|
}
|
|
1211
1287
|
}
|
|
1212
1288
|
}
|
|
@@ -1219,6 +1295,7 @@ newItems are ordered earliest to latest (as they are returned from the worker)
|
|
|
1219
1295
|
*/
|
|
1220
1296
|
let handleQueryResult = (
|
|
1221
1297
|
fetchState: t,
|
|
1298
|
+
~indexingAddresses: IndexingAddresses.t,
|
|
1222
1299
|
~query: query,
|
|
1223
1300
|
~latestFetchedBlock: blockNumberAndTimestamp,
|
|
1224
1301
|
~newItems,
|
|
@@ -1229,9 +1306,10 @@ let handleQueryResult = (
|
|
|
1229
1306
|
// param-level analogue of EventRouter's srcAddress effectiveStartBlock check.
|
|
1230
1307
|
let newItems = newItems->Array.filter(item =>
|
|
1231
1308
|
switch item {
|
|
1232
|
-
| Internal.Event({
|
|
1233
|
-
switch
|
|
1234
|
-
| Some(filter) =>
|
|
1309
|
+
| Internal.Event({onEventRegistration, payload, blockNumber}) =>
|
|
1310
|
+
switch onEventRegistration.clientAddressFilter {
|
|
1311
|
+
| Some(filter) =>
|
|
1312
|
+
filter(payload, blockNumber, indexingAddresses->IndexingAddresses.rawForFilter)
|
|
1235
1313
|
| None => true
|
|
1236
1314
|
}
|
|
1237
1315
|
| _ => true
|
|
@@ -1241,6 +1319,7 @@ let handleQueryResult = (
|
|
|
1241
1319
|
~optimizedPartitions=fetchState.optimizedPartitions->OptimizedPartitions.handleQueryResponse(
|
|
1242
1320
|
~query,
|
|
1243
1321
|
~knownHeight=fetchState.knownHeight,
|
|
1322
|
+
~itemsCount=newItems->Array.length,
|
|
1244
1323
|
~latestFetchedBlock,
|
|
1245
1324
|
),
|
|
1246
1325
|
~mutItems=?{
|
|
@@ -1253,7 +1332,6 @@ let handleQueryResult = (
|
|
|
1253
1332
|
}
|
|
1254
1333
|
|
|
1255
1334
|
type nextQuery =
|
|
1256
|
-
| ReachedMaxConcurrency
|
|
1257
1335
|
| WaitingForNewBlock
|
|
1258
1336
|
| NothingToQuery
|
|
1259
1337
|
| Ready(array<query>)
|
|
@@ -1267,6 +1345,8 @@ let startFetchingQueries = ({optimizedPartitions}: t, ~queries: array<query>) =>
|
|
|
1267
1345
|
fromBlock: q.fromBlock,
|
|
1268
1346
|
toBlock: q.toBlock,
|
|
1269
1347
|
isChunk: q.isChunk,
|
|
1348
|
+
itemsTarget: q.itemsTarget,
|
|
1349
|
+
itemsEst: q.itemsEst,
|
|
1270
1350
|
fetchedBlock: None,
|
|
1271
1351
|
}
|
|
1272
1352
|
|
|
@@ -1287,141 +1367,218 @@ let startFetchingQueries = ({optimizedPartitions}: t, ~queries: array<query>) =>
|
|
|
1287
1367
|
}
|
|
1288
1368
|
}
|
|
1289
1369
|
|
|
1290
|
-
|
|
1291
|
-
let
|
|
1370
|
+
// Most parallel in-flight chunk queries a single partition may have at once.
|
|
1371
|
+
let maxPendingChunksPerPartition = 12
|
|
1372
|
+
|
|
1373
|
+
// Fills a gap range (a hole left between completed/pending chunks, e.g. from an
|
|
1374
|
+
// out-of-order partial response) unconditionally — gaps are already-committed
|
|
1375
|
+
// range, not subject to this tick's water-fill budget. Priced by the
|
|
1376
|
+
// partition's trusted density when it has one; otherwise by the "available
|
|
1377
|
+
// density" — the partition's equal-divide budget spread over its remaining
|
|
1378
|
+
// range this tick — so a small gap reserves proportionally little instead of a
|
|
1379
|
+
// noisy one-sample estimate. Chunks only on a trusted POSITIVE density, same
|
|
1380
|
+
// rule as the water-fill. Returns the created queries' total itemsEst.
|
|
1381
|
+
let pushGapFillQueries = (
|
|
1292
1382
|
queries: array<query>,
|
|
1293
1383
|
~partitionId: string,
|
|
1294
1384
|
~rangeFromBlock: int,
|
|
1295
1385
|
~rangeEndBlock: option<int>,
|
|
1296
|
-
~
|
|
1386
|
+
~knownHeight: int,
|
|
1387
|
+
~chainTargetBlock: int,
|
|
1297
1388
|
~maybeChunkRange: option<int>,
|
|
1389
|
+
~maxChunks: int,
|
|
1390
|
+
~partition: partition,
|
|
1391
|
+
~partitionBudget: float,
|
|
1392
|
+
~chunkItemsMultiplier: float,
|
|
1298
1393
|
~selection: selection,
|
|
1299
1394
|
~addressesByContractName: dict<array<Address.t>>,
|
|
1300
|
-
~indexingAddresses: dict<indexingAddress>,
|
|
1301
1395
|
) => {
|
|
1302
|
-
|
|
1396
|
+
let cost = ref(0.)
|
|
1397
|
+
|
|
1398
|
+
// Gaps past the chain's target block wait: they regenerate from the
|
|
1399
|
+
// pending-walk each tick and fill once the target reaches them.
|
|
1400
|
+
if rangeFromBlock <= Pervasives.min(knownHeight, chainTargetBlock) && maxChunks > 0 {
|
|
1303
1401
|
switch rangeEndBlock {
|
|
1304
1402
|
| Some(endBlock) if rangeFromBlock > endBlock => ()
|
|
1305
1403
|
| _ =>
|
|
1306
|
-
|
|
1307
|
-
|
|
1404
|
+
let trustedDensity = partition->getTrustedDensity
|
|
1405
|
+
let maxBlock = switch rangeEndBlock {
|
|
1406
|
+
| Some(eb) => eb
|
|
1407
|
+
| None => chainTargetBlock
|
|
1408
|
+
}
|
|
1409
|
+
let pushSingleQuery = (~density, ~isChunk) => {
|
|
1410
|
+
let itemsTarget = densityItemsTarget(
|
|
1411
|
+
~density=density *. chunkItemsMultiplier,
|
|
1412
|
+
~fromBlock=rangeFromBlock,
|
|
1413
|
+
~toBlock=rangeEndBlock,
|
|
1414
|
+
~chainTargetBlock,
|
|
1415
|
+
)
|
|
1416
|
+
let itemsEst = densityItemsTarget(
|
|
1417
|
+
~density,
|
|
1418
|
+
~fromBlock=rangeFromBlock,
|
|
1419
|
+
~toBlock=rangeEndBlock,
|
|
1420
|
+
~chainTargetBlock,
|
|
1421
|
+
)
|
|
1308
1422
|
queries->Array.push({
|
|
1309
1423
|
partitionId,
|
|
1310
1424
|
fromBlock: rangeFromBlock,
|
|
1311
1425
|
toBlock: rangeEndBlock,
|
|
1312
1426
|
selection,
|
|
1313
|
-
isChunk
|
|
1427
|
+
isChunk,
|
|
1428
|
+
itemsTarget,
|
|
1429
|
+
itemsEst,
|
|
1314
1430
|
addressesByContractName,
|
|
1315
|
-
indexingAddresses,
|
|
1316
1431
|
})
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
}
|
|
1432
|
+
cost := cost.contents +. itemsEst->Int.toFloat
|
|
1433
|
+
}
|
|
1434
|
+
switch (trustedDensity, maybeChunkRange) {
|
|
1435
|
+
| (Some(density), Some(chunkRange)) if density > 0. =>
|
|
1322
1436
|
let chunkSize = Js.Math.ceil_int(chunkRange->Int.toFloat *. 1.8)
|
|
1323
|
-
|
|
1324
|
-
// quickly and refresh the chunking heuristic, then three full-size chunks.
|
|
1325
|
-
let probeSize = Js.Math.ceil_int(chunkRange->Int.toFloat *. 0.9)
|
|
1326
|
-
let getChunkSize = chunkIdx => chunkIdx < 2 ? probeSize : chunkSize
|
|
1327
|
-
if rangeFromBlock + getChunkSize(0) + getChunkSize(1) - 1 <= maxBlock {
|
|
1437
|
+
if rangeFromBlock + chunkSize * 2 - 1 <= maxBlock {
|
|
1328
1438
|
let chunkFromBlock = ref(rangeFromBlock)
|
|
1329
1439
|
let chunkIdx = ref(0)
|
|
1330
1440
|
while (
|
|
1331
|
-
chunkIdx.contents <
|
|
1332
|
-
chunkFromBlock.contents + getChunkSize(chunkIdx.contents) - 1 <= maxBlock
|
|
1441
|
+
chunkIdx.contents < maxChunks && chunkFromBlock.contents + chunkSize - 1 <= maxBlock
|
|
1333
1442
|
) {
|
|
1334
|
-
let chunkToBlock = chunkFromBlock.contents +
|
|
1443
|
+
let chunkToBlock = chunkFromBlock.contents + chunkSize - 1
|
|
1444
|
+
let itemsTarget = densityItemsTarget(
|
|
1445
|
+
~density=density *. chunkItemsMultiplier,
|
|
1446
|
+
~fromBlock=chunkFromBlock.contents,
|
|
1447
|
+
~toBlock=Some(chunkToBlock),
|
|
1448
|
+
~chainTargetBlock,
|
|
1449
|
+
)
|
|
1450
|
+
let itemsEst = densityItemsTarget(
|
|
1451
|
+
~density,
|
|
1452
|
+
~fromBlock=chunkFromBlock.contents,
|
|
1453
|
+
~toBlock=Some(chunkToBlock),
|
|
1454
|
+
~chainTargetBlock,
|
|
1455
|
+
)
|
|
1335
1456
|
queries->Array.push({
|
|
1336
1457
|
partitionId,
|
|
1337
1458
|
fromBlock: chunkFromBlock.contents,
|
|
1338
1459
|
toBlock: Some(chunkToBlock),
|
|
1339
1460
|
isChunk: true,
|
|
1340
1461
|
selection,
|
|
1462
|
+
itemsTarget,
|
|
1463
|
+
itemsEst,
|
|
1341
1464
|
addressesByContractName,
|
|
1342
|
-
indexingAddresses,
|
|
1343
1465
|
})
|
|
1466
|
+
cost := cost.contents +. itemsEst->Int.toFloat
|
|
1344
1467
|
chunkFromBlock := chunkToBlock + 1
|
|
1345
1468
|
chunkIdx := chunkIdx.contents + 1
|
|
1346
1469
|
}
|
|
1347
1470
|
} else {
|
|
1348
1471
|
// Not enough room for 2 chunks, fall back to a single query
|
|
1349
|
-
|
|
1350
|
-
partitionId,
|
|
1351
|
-
fromBlock: rangeFromBlock,
|
|
1352
|
-
toBlock: rangeEndBlock,
|
|
1353
|
-
selection,
|
|
1354
|
-
isChunk: rangeEndBlock !== None,
|
|
1355
|
-
addressesByContractName,
|
|
1356
|
-
indexingAddresses,
|
|
1357
|
-
})
|
|
1472
|
+
pushSingleQuery(~density, ~isChunk=rangeEndBlock !== None)
|
|
1358
1473
|
}
|
|
1474
|
+
| (Some(density), _) => pushSingleQuery(~density, ~isChunk=false)
|
|
1475
|
+
| (None, _) =>
|
|
1476
|
+
let remainingRange = Pervasives.max(1, chainTargetBlock - rangeFromBlock + 1)
|
|
1477
|
+
pushSingleQuery(~density=partitionBudget /. remainingRange->Int.toFloat, ~isChunk=false)
|
|
1359
1478
|
}
|
|
1360
1479
|
}
|
|
1361
1480
|
}
|
|
1481
|
+
cost.contents
|
|
1362
1482
|
}
|
|
1363
1483
|
|
|
1364
|
-
//
|
|
1365
|
-
|
|
1484
|
+
// The level every partition ends at when `budget` is poured across partitions
|
|
1485
|
+
// already holding `footprints`: the unique L with Σ max(0, L - fᵢ) = budget.
|
|
1486
|
+
// Partitions above L get nothing (their head start is their share); the rest
|
|
1487
|
+
// are topped up exactly to L, so the pour equals the budget no matter how
|
|
1488
|
+
// uneven the footprints are.
|
|
1489
|
+
let waterLevel = (~budget: float, ~footprints: array<float>) => {
|
|
1490
|
+
let sorted = footprints->Array.toSorted(Float.compare)
|
|
1491
|
+
let n = sorted->Array.length
|
|
1492
|
+
let prefix = ref(0.)
|
|
1493
|
+
let level = ref(None)
|
|
1494
|
+
let idx = ref(0)
|
|
1495
|
+
while level.contents == None && idx.contents < n {
|
|
1496
|
+
let i = idx.contents
|
|
1497
|
+
prefix := prefix.contents +. sorted->Array.getUnsafe(i)
|
|
1498
|
+
// The level if only the i+1 lowest footprints receive water — correct once
|
|
1499
|
+
// it doesn't reach the next footprint up.
|
|
1500
|
+
let candidate = (budget +. prefix.contents) /. (i + 1)->Int.toFloat
|
|
1501
|
+
if i == n - 1 || candidate <= sorted->Array.getUnsafe(i + 1) {
|
|
1502
|
+
level := Some(candidate)
|
|
1503
|
+
}
|
|
1504
|
+
idx := idx.contents + 1
|
|
1505
|
+
}
|
|
1506
|
+
level.contents->Option.getOr(0.)
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
// Per-partition mutable state threaded through the water-fill rounds below.
|
|
1510
|
+
type waterFillState = {
|
|
1511
|
+
partitionId: string,
|
|
1512
|
+
p: partition,
|
|
1513
|
+
mutable cursor: int,
|
|
1514
|
+
mutable chunksUsedThisCall: int,
|
|
1515
|
+
// Existing mutPendingQueries count before this call — fixed for the call,
|
|
1516
|
+
// used with chunksUsedThisCall against maxPendingChunksPerPartition.
|
|
1517
|
+
pendingCount: int,
|
|
1518
|
+
queryEndBlock: option<int>,
|
|
1519
|
+
maybeChunkRange: option<int>,
|
|
1520
|
+
// This partition's own slot in queriesByPartitionIndex — gap-fill and every
|
|
1521
|
+
// water-fill round push here directly, so query order falls out of
|
|
1522
|
+
// idsInAscOrder for free (see getNextQuery) instead of a final sort pass.
|
|
1523
|
+
bucket: array<query>,
|
|
1524
|
+
}
|
|
1366
1525
|
|
|
1526
|
+
// Candidate queries are sized against chainTargetBlock, the soft querying
|
|
1527
|
+
// horizon the owning chain wants to reach this tick — derived by ChainState
|
|
1528
|
+
// from its share of the indexer-wide buffer budget and its chain-level event
|
|
1529
|
+
// density. chainTargetBlock is never used as a hard query end, only to (a)
|
|
1530
|
+
// select which partitions are "in range" this tick and (b) size an open-ended
|
|
1531
|
+
// query with no other ceiling: the true hard bounds stay
|
|
1532
|
+
// endBlock/mergeBlock/the lagged head.
|
|
1533
|
+
//
|
|
1534
|
+
// In-range partitions share chainTargetItems (this chain's target total
|
|
1535
|
+
// footprint — in-flight plus new) by water-fill: each round pours exactly the
|
|
1536
|
+
// remaining fresh budget at the level computed by waterLevel, so a partition
|
|
1537
|
+
// already holding more than the level (from earlier ticks' in-flight queries)
|
|
1538
|
+
// gets nothing and its implicit share flows to the others, while totals stay
|
|
1539
|
+
// even and the pour never exceeds the budget. Rounds repeat only because
|
|
1540
|
+
// emits are quantized: a partition may consume less than its allotment (range
|
|
1541
|
+
// or chunk-cap runs out) or slightly more (min one chunk), and the leftover
|
|
1542
|
+
// re-pours over whoever still has range left.
|
|
1543
|
+
//
|
|
1544
|
+
// A partition with a trusted positive density (two or more responses — see
|
|
1545
|
+
// getMinHistoryRange) always emits at least one full-size chunk/query once
|
|
1546
|
+
// given an allotment, sized by its real density — may overshoot the
|
|
1547
|
+
// allotment by at most one chunk (the server also enforces itemsTarget via a
|
|
1548
|
+
// maxNumLogs-style cap, so an overshoot truncates the response rather than
|
|
1549
|
+
// the buffer). Any other partition (no signal, one noisy sample, or a
|
|
1550
|
+
// density-0 estimate) emits one open-ended probe sized exactly at its
|
|
1551
|
+
// allotment instead.
|
|
1367
1552
|
let getNextQuery = (
|
|
1368
|
-
{
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
blockLag,
|
|
1373
|
-
latestOnBlockBlockNumber,
|
|
1374
|
-
knownHeight,
|
|
1375
|
-
} as fetchState: t,
|
|
1376
|
-
~concurrencyLimit,
|
|
1377
|
-
~bufferLimit,
|
|
1553
|
+
{optimizedPartitions, blockLag, latestOnBlockBlockNumber, knownHeight, endBlock}: t,
|
|
1554
|
+
~chainTargetBlock: int,
|
|
1555
|
+
~chainTargetItems: float,
|
|
1556
|
+
~chunkItemsMultiplier: float=1.,
|
|
1378
1557
|
) => {
|
|
1379
1558
|
let headBlockNumber = knownHeight - blockLag
|
|
1380
1559
|
if headBlockNumber <= 0 {
|
|
1381
1560
|
WaitingForNewBlock
|
|
1382
|
-
} else if concurrencyLimit === 0 {
|
|
1383
|
-
ReachedMaxConcurrency
|
|
1384
|
-
} else if bufferLimit <= 0 {
|
|
1385
|
-
// No room left in the shared buffer pool for this chain; wait for processing
|
|
1386
|
-
// to drain before fetching more.
|
|
1387
|
-
NothingToQuery
|
|
1388
1561
|
} else {
|
|
1389
1562
|
let isOnBlockBehindTheHead = latestOnBlockBlockNumber < headBlockNumber
|
|
1390
1563
|
let shouldWaitForNewBlock = ref(
|
|
1391
|
-
switch
|
|
1564
|
+
switch endBlock {
|
|
1392
1565
|
| Some(endBlock) => headBlockNumber < endBlock
|
|
1393
1566
|
| None => true
|
|
1394
1567
|
} &&
|
|
1395
1568
|
!isOnBlockBehindTheHead,
|
|
1396
1569
|
)
|
|
1397
1570
|
|
|
1398
|
-
// Limit how far ahead we fetch to bufferLimit items (this chain's share of
|
|
1399
|
-
// the indexer-wide buffer pool) so processing always has buffer without
|
|
1400
|
-
// ballooning memory. A partition that fetched further is skipped until the
|
|
1401
|
-
// buffer drains.
|
|
1402
|
-
let maxQueryBlockNumber = {
|
|
1403
|
-
switch buffer->Array.get(bufferLimit - 1) {
|
|
1404
|
-
| Some(item) =>
|
|
1405
|
-
// Just in case check that we don't query beyond the current block
|
|
1406
|
-
Pervasives.min(item->Internal.getItemBlockNumber, knownHeight)
|
|
1407
|
-
| None => knownHeight
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1410
|
-
|
|
1411
|
-
let queries = []
|
|
1412
|
-
|
|
1413
1571
|
let partitionsCount = optimizedPartitions.idsInAscOrder->Array.length
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1572
|
+
|
|
1573
|
+
// Every partition is visited once here regardless of whether it gets a
|
|
1574
|
+
// query pushed below — waiting-for-new-block bookkeeping shouldn't depend
|
|
1575
|
+
// on this tick's budget.
|
|
1576
|
+
for idx in 0 to partitionsCount - 1 {
|
|
1417
1577
|
let partitionId = optimizedPartitions.idsInAscOrder->Array.getUnsafe(idx)
|
|
1418
1578
|
let p = optimizedPartitions.entities->Dict.getUnsafe(partitionId)
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
let hasPendingQueries = pendingCount > 0
|
|
1423
|
-
|
|
1424
|
-
if hasPendingQueries || isBehindTheHead {
|
|
1579
|
+
if (
|
|
1580
|
+
p.mutPendingQueries->Array.length > 0 || p.latestFetchedBlock.blockNumber < headBlockNumber
|
|
1581
|
+
) {
|
|
1425
1582
|
// Even if there are some partitions waiting for the new block
|
|
1426
1583
|
// We still want to wait for all partitions reaching the head
|
|
1427
1584
|
// because they might update knownHeight in their response
|
|
@@ -1429,50 +1586,86 @@ let getNextQuery = (
|
|
|
1429
1586
|
// and we don't want to poll the head for a few small partitions
|
|
1430
1587
|
shouldWaitForNewBlock := false
|
|
1431
1588
|
}
|
|
1589
|
+
}
|
|
1432
1590
|
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1591
|
+
// One bucket per partition, in idsInAscOrder order — gap-fill and
|
|
1592
|
+
// water-fill both push into a partition's own bucket, so flattening at
|
|
1593
|
+
// the end (see below) reproduces idsInAscOrder without a sort.
|
|
1594
|
+
let queriesByPartitionIndex: array<
|
|
1595
|
+
array<query>,
|
|
1596
|
+
> = Array.fromInitializer(~length=partitionsCount, _ => [])
|
|
1597
|
+
|
|
1598
|
+
// Compute queryEndBlock for this partition
|
|
1599
|
+
let computeQueryEndBlock = (p: partition) => {
|
|
1600
|
+
let queryEndBlock = Utils.Math.minOptInt(endBlock, p.mergeBlock)
|
|
1601
|
+
switch blockLag {
|
|
1438
1602
|
| 0 => queryEndBlock
|
|
1439
1603
|
| _ =>
|
|
1440
1604
|
// Force head block as an endBlock when blockLag is set
|
|
1441
1605
|
// because otherwise HyperSync might return bigger range
|
|
1442
1606
|
Utils.Math.minOptInt(Some(headBlockNumber), queryEndBlock)
|
|
1443
1607
|
}
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
// Each partition's existing in-flight itemsEst, summed once and reused
|
|
1611
|
+
// both for chainReserved (the call-wide fresh-budget ceiling below) and to
|
|
1612
|
+
// seed each partition's water-fill footprint — so a partition already
|
|
1613
|
+
// holding in-flight queries is counted toward its even share and doesn't
|
|
1614
|
+
// get topped up past the others.
|
|
1615
|
+
let existingReservedByPartition = Dict.make()
|
|
1616
|
+
let chainReserved = ref(0.)
|
|
1617
|
+
for idx in 0 to partitionsCount - 1 {
|
|
1618
|
+
let partitionId = optimizedPartitions.idsInAscOrder->Array.getUnsafe(idx)
|
|
1619
|
+
let p = optimizedPartitions.entities->Dict.getUnsafe(partitionId)
|
|
1620
|
+
let cost = ref(0.)
|
|
1621
|
+
for pqIdx in 0 to p.mutPendingQueries->Array.length - 1 {
|
|
1622
|
+
cost := cost.contents +. (p.mutPendingQueries->Array.getUnsafe(pqIdx)).itemsEst->Int.toFloat
|
|
1452
1623
|
}
|
|
1624
|
+
existingReservedByPartition->Dict.set(partitionId, cost.contents)
|
|
1625
|
+
chainReserved := chainReserved.contents +. cost.contents
|
|
1626
|
+
}
|
|
1453
1627
|
|
|
1628
|
+
// Phase A: gap-fill. Walk each partition's pending queries once,
|
|
1629
|
+
// unconditionally filling any hole (e.g. from an out-of-order partial
|
|
1630
|
+
// chunk response) — uncapped, same as before this tick's water-fill was
|
|
1631
|
+
// introduced. This also determines each partition's post-gap cursor and
|
|
1632
|
+
// whether it's blocked on an unresolved single-shot query.
|
|
1633
|
+
let fillStates = []
|
|
1634
|
+
let gapFillCost = ref(0.)
|
|
1635
|
+
for idx in 0 to partitionsCount - 1 {
|
|
1636
|
+
let partitionId = optimizedPartitions.idsInAscOrder->Array.getUnsafe(idx)
|
|
1637
|
+
let p = optimizedPartitions.entities->Dict.getUnsafe(partitionId)
|
|
1638
|
+
let bucket = queriesByPartitionIndex->Array.getUnsafe(idx)
|
|
1639
|
+
let pendingCount = p.mutPendingQueries->Array.length
|
|
1640
|
+
let queryEndBlock = computeQueryEndBlock(p)
|
|
1454
1641
|
let maybeChunkRange = getMinHistoryRange(p)
|
|
1455
1642
|
|
|
1456
|
-
// Walk pending queries to find open ranges and create queries for each
|
|
1457
1643
|
let cursor = ref(p.latestFetchedBlock.blockNumber + 1)
|
|
1458
1644
|
let canContinue = ref(true)
|
|
1645
|
+
let chunksUsedThisCall = ref(0)
|
|
1459
1646
|
let pqIdx = ref(0)
|
|
1460
|
-
while pqIdx.contents <
|
|
1647
|
+
while pqIdx.contents < pendingCount && canContinue.contents {
|
|
1461
1648
|
let pq = p.mutPendingQueries->Array.getUnsafe(pqIdx.contents)
|
|
1462
1649
|
|
|
1463
|
-
// Gap before this pending query → create queries for the gap range
|
|
1464
1650
|
if pq.fromBlock > cursor.contents {
|
|
1465
|
-
|
|
1466
|
-
|
|
1651
|
+
let beforeLen = bucket->Array.length
|
|
1652
|
+
let cost = pushGapFillQueries(
|
|
1653
|
+
bucket,
|
|
1467
1654
|
~partitionId,
|
|
1468
1655
|
~rangeFromBlock=cursor.contents,
|
|
1469
1656
|
~rangeEndBlock=Utils.Math.minOptInt(Some(pq.fromBlock - 1), queryEndBlock),
|
|
1470
|
-
~
|
|
1657
|
+
~knownHeight,
|
|
1658
|
+
~chainTargetBlock,
|
|
1471
1659
|
~maybeChunkRange,
|
|
1660
|
+
~maxChunks=maxPendingChunksPerPartition - pendingCount - chunksUsedThisCall.contents,
|
|
1661
|
+
~partition=p,
|
|
1662
|
+
~partitionBudget=chainTargetItems /. partitionsCount->Int.toFloat,
|
|
1663
|
+
~chunkItemsMultiplier,
|
|
1472
1664
|
~selection=p.selection,
|
|
1473
1665
|
~addressesByContractName=p.addressesByContractName,
|
|
1474
|
-
~indexingAddresses,
|
|
1475
1666
|
)
|
|
1667
|
+
chunksUsedThisCall := chunksUsedThisCall.contents + (bucket->Array.length - beforeLen)
|
|
1668
|
+
gapFillCost := gapFillCost.contents +. cost
|
|
1476
1669
|
}
|
|
1477
1670
|
switch pq {
|
|
1478
1671
|
| {isChunk: true, toBlock: Some(toBlock), fetchedBlock: Some({blockNumber})}
|
|
@@ -1484,40 +1677,180 @@ let getNextQuery = (
|
|
|
1484
1677
|
pqIdx := pqIdx.contents + 1
|
|
1485
1678
|
}
|
|
1486
1679
|
|
|
1487
|
-
// Tail range after all pending queries
|
|
1488
1680
|
if canContinue.contents {
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
)
|
|
1681
|
+
fillStates
|
|
1682
|
+
->Array.push({
|
|
1683
|
+
partitionId,
|
|
1684
|
+
p,
|
|
1685
|
+
cursor: cursor.contents,
|
|
1686
|
+
chunksUsedThisCall: chunksUsedThisCall.contents,
|
|
1687
|
+
pendingCount,
|
|
1688
|
+
queryEndBlock,
|
|
1689
|
+
maybeChunkRange,
|
|
1690
|
+
bucket,
|
|
1691
|
+
})
|
|
1692
|
+
->ignore
|
|
1500
1693
|
}
|
|
1694
|
+
}
|
|
1501
1695
|
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1696
|
+
// Fresh budget for this call — what's left of chainTargetItems after
|
|
1697
|
+
// existing in-flight queries and this tick's gap-fill. The water-fill
|
|
1698
|
+
// rounds below hand it out; a partition already holding a large share
|
|
1699
|
+
// (seeded below) gets proportionally less so totals stay even.
|
|
1700
|
+
let rangeItemsTarget = Pervasives.max(
|
|
1701
|
+
0.,
|
|
1702
|
+
chainTargetItems -. chainReserved.contents -. gapFillCost.contents,
|
|
1703
|
+
)
|
|
1704
|
+
|
|
1705
|
+
// Each in-range partition's running footprint: existing in-flight plus any
|
|
1706
|
+
// gap-fill just pushed into its bucket. The water-fill levels every
|
|
1707
|
+
// partition up toward a shared line, so this seed is what a partition
|
|
1708
|
+
// starts the fill already holding.
|
|
1709
|
+
let reservedByPartition = Dict.make()
|
|
1710
|
+
fillStates->Array.forEach(fs => {
|
|
1711
|
+
let cost = ref(existingReservedByPartition->Dict.getUnsafe(fs.partitionId))
|
|
1712
|
+
for i in 0 to fs.bucket->Array.length - 1 {
|
|
1713
|
+
cost := cost.contents +. (fs.bucket->Array.getUnsafe(i)).itemsEst->Int.toFloat
|
|
1516
1714
|
}
|
|
1715
|
+
reservedByPartition->Dict.set(fs.partitionId, cost.contents)
|
|
1716
|
+
})
|
|
1517
1717
|
|
|
1518
|
-
|
|
1718
|
+
let isInRange = (fs: waterFillState) =>
|
|
1719
|
+
fs.cursor <= chainTargetBlock &&
|
|
1720
|
+
switch fs.queryEndBlock {
|
|
1721
|
+
| Some(eb) => fs.cursor <= eb
|
|
1722
|
+
| None => true
|
|
1723
|
+
} &&
|
|
1724
|
+
fs.pendingCount + fs.chunksUsedThisCall < maxPendingChunksPerPartition
|
|
1725
|
+
|
|
1726
|
+
let inRangeStates = fillStates->Array.filter(isInRange)
|
|
1727
|
+
|
|
1728
|
+
// Emits this round's queries for one partition, given its water-fill
|
|
1729
|
+
// allotment. Mutates fs.cursor/chunksUsedThisCall and returns the
|
|
1730
|
+
// itemsEst consumed.
|
|
1731
|
+
//
|
|
1732
|
+
// Chunks require a POSITIVE trusted density: density 0 prices every chunk
|
|
1733
|
+
// at ~nothing, letting a partition flood its full chunk pipeline with
|
|
1734
|
+
// hard-bounded queries that crawl 1.8× per two responses — an open-ended
|
|
1735
|
+
// probe instead gets the server's full scan range in one response.
|
|
1736
|
+
let emitQueries = (fs: waterFillState, ~budget: float) => {
|
|
1737
|
+
let p = fs.p
|
|
1738
|
+
let maxBlock = switch fs.queryEndBlock {
|
|
1739
|
+
| Some(eb) => eb
|
|
1740
|
+
| None => chainTargetBlock
|
|
1741
|
+
}
|
|
1742
|
+
switch (fs.maybeChunkRange, p->getTrustedDensity) {
|
|
1743
|
+
| (Some(minHistoryRange), Some(density)) if density > 0. =>
|
|
1744
|
+
// Chunking active: strict chunks with a hard endBlock, sized by real
|
|
1745
|
+
// density with the chunk headroom multiplier — at least one full
|
|
1746
|
+
// chunk this round even if the allotment falls short.
|
|
1747
|
+
let chunkSize = Js.Math.ceil_int(minHistoryRange->Int.toFloat *. 1.8)
|
|
1748
|
+
let maxChunksRemaining =
|
|
1749
|
+
maxPendingChunksPerPartition - fs.pendingCount - fs.chunksUsedThisCall
|
|
1750
|
+
let consumed = ref(0.)
|
|
1751
|
+
let created = ref(0)
|
|
1752
|
+
let chunkFromBlock = ref(fs.cursor)
|
|
1753
|
+
let budgetLeft = ref(true)
|
|
1754
|
+
// No chunk starts past chainTargetBlock; an emitted chunk still keeps
|
|
1755
|
+
// its full span (chunkToBlock may exceed the target — only
|
|
1756
|
+
// endBlock/mergeBlock are hard bounds).
|
|
1757
|
+
while (
|
|
1758
|
+
budgetLeft.contents &&
|
|
1759
|
+
created.contents < maxChunksRemaining &&
|
|
1760
|
+
chunkFromBlock.contents <= Pervasives.min(maxBlock, chainTargetBlock)
|
|
1761
|
+
) {
|
|
1762
|
+
let chunkToBlock = Pervasives.min(chunkFromBlock.contents + chunkSize - 1, maxBlock)
|
|
1763
|
+
let rawEst = density *. (chunkToBlock - chunkFromBlock.contents + 1)->Int.toFloat
|
|
1764
|
+
let itemsEst = Pervasives.max(1, rawEst->Math.ceil->Float.toInt)
|
|
1765
|
+
let itemsTarget = Pervasives.max(
|
|
1766
|
+
1,
|
|
1767
|
+
(rawEst *. chunkItemsMultiplier)->Math.ceil->Float.toInt,
|
|
1768
|
+
)
|
|
1769
|
+
if consumed.contents == 0. || consumed.contents +. itemsEst->Int.toFloat <= budget {
|
|
1770
|
+
fs.bucket->Array.push({
|
|
1771
|
+
partitionId: fs.partitionId,
|
|
1772
|
+
fromBlock: chunkFromBlock.contents,
|
|
1773
|
+
toBlock: Some(chunkToBlock),
|
|
1774
|
+
isChunk: true,
|
|
1775
|
+
selection: p.selection,
|
|
1776
|
+
itemsTarget,
|
|
1777
|
+
itemsEst,
|
|
1778
|
+
addressesByContractName: p.addressesByContractName,
|
|
1779
|
+
})
|
|
1780
|
+
consumed := consumed.contents +. itemsEst->Int.toFloat
|
|
1781
|
+
chunkFromBlock := chunkToBlock + 1
|
|
1782
|
+
created := created.contents + 1
|
|
1783
|
+
} else {
|
|
1784
|
+
budgetLeft := false
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
fs.cursor = chunkFromBlock.contents
|
|
1788
|
+
fs.chunksUsedThisCall = fs.chunksUsedThisCall + created.contents
|
|
1789
|
+
consumed.contents
|
|
1790
|
+
| _ =>
|
|
1791
|
+
let itemsTarget = Pervasives.max(1, Math.round(budget)->Float.toInt)
|
|
1792
|
+
let itemsEst = itemsTarget
|
|
1793
|
+
fs.bucket->Array.push({
|
|
1794
|
+
partitionId: fs.partitionId,
|
|
1795
|
+
fromBlock: fs.cursor,
|
|
1796
|
+
toBlock: fs.queryEndBlock,
|
|
1797
|
+
isChunk: false,
|
|
1798
|
+
selection: p.selection,
|
|
1799
|
+
itemsTarget,
|
|
1800
|
+
itemsEst,
|
|
1801
|
+
addressesByContractName: p.addressesByContractName,
|
|
1802
|
+
})
|
|
1803
|
+
fs.cursor = maxBlock + 1
|
|
1804
|
+
fs.chunksUsedThisCall = fs.chunksUsedThisCall + 1
|
|
1805
|
+
itemsEst->Int.toFloat
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
// Water-fill. Range membership is fixed by chainTargetBlock/queryEndBlock,
|
|
1810
|
+
// so the in-range partitions are filtered once up front; each round pours
|
|
1811
|
+
// the remaining fresh budget at the waterLevel of the still-not-filled
|
|
1812
|
+
// partitions' footprints and keeps only those still in range for the next
|
|
1813
|
+
// round. Allotments sum to exactly the poured budget, so the outcome is
|
|
1814
|
+
// order-independent and never exceeds it — the only overshoot left is the
|
|
1815
|
+
// min-one-chunk quantization in emitQueries, bounded by one chunk per
|
|
1816
|
+
// partition per round.
|
|
1817
|
+
//
|
|
1818
|
+
// No explicit round cap: every emit advances chunksUsedThisCall (capped at
|
|
1819
|
+
// maxPendingChunksPerPartition) and consumes a positive amount of fresh
|
|
1820
|
+
// budget, so the not-filled set drains on its own and the loop also stops
|
|
1821
|
+
// once the whole budget is poured.
|
|
1822
|
+
let notFilledPartitions = ref(inRangeStates)
|
|
1823
|
+
let remainingBudget = ref(rangeItemsTarget)
|
|
1824
|
+
while notFilledPartitions.contents->Array.length > 0 && remainingBudget.contents > 0. {
|
|
1825
|
+
let level = waterLevel(
|
|
1826
|
+
~budget=remainingBudget.contents,
|
|
1827
|
+
~footprints=notFilledPartitions.contents->Array.map(fs =>
|
|
1828
|
+
reservedByPartition->Dict.getUnsafe(fs.partitionId)
|
|
1829
|
+
),
|
|
1830
|
+
)
|
|
1831
|
+
let next = []
|
|
1832
|
+
notFilledPartitions.contents->Array.forEach(fs => {
|
|
1833
|
+
let reserved = reservedByPartition->Dict.getUnsafe(fs.partitionId)
|
|
1834
|
+
let budget = level -. reserved
|
|
1835
|
+
if budget > 0. {
|
|
1836
|
+
let consumed = emitQueries(fs, ~budget)
|
|
1837
|
+
reservedByPartition->Dict.set(fs.partitionId, reserved +. consumed)
|
|
1838
|
+
remainingBudget := remainingBudget.contents -. consumed
|
|
1839
|
+
|
|
1840
|
+
if fs->isInRange {
|
|
1841
|
+
next->Array.push(fs)->ignore
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
})
|
|
1845
|
+
notFilledPartitions := next
|
|
1519
1846
|
}
|
|
1520
1847
|
|
|
1848
|
+
// Each partition pushed only into its own bucket (indexed by its
|
|
1849
|
+
// idsInAscOrder position), so flattening reproduces idsInAscOrder order
|
|
1850
|
+
// directly — no sort needed even though water-fill rounds interleave
|
|
1851
|
+
// across partitions.
|
|
1852
|
+
let queries = queriesByPartitionIndex->Array.flat
|
|
1853
|
+
|
|
1521
1854
|
if queries->Utils.Array.isEmpty {
|
|
1522
1855
|
if shouldWaitForNewBlock.contents {
|
|
1523
1856
|
WaitingForNewBlock
|
|
@@ -1525,13 +1858,6 @@ let getNextQuery = (
|
|
|
1525
1858
|
NothingToQuery
|
|
1526
1859
|
}
|
|
1527
1860
|
} else {
|
|
1528
|
-
// Enforce concurrency limit: sort by fromBlock and take the first concurrencyLimit
|
|
1529
|
-
let queries = if queries->Array.length > concurrencyLimit {
|
|
1530
|
-
queries->Array.sort((a, b) => Int.compare(a.fromBlock, b.fromBlock))
|
|
1531
|
-
queries->Array.slice(~start=0, ~end=concurrencyLimit)
|
|
1532
|
-
} else {
|
|
1533
|
-
queries
|
|
1534
|
-
}
|
|
1535
1861
|
Ready(queries)
|
|
1536
1862
|
}
|
|
1537
1863
|
}
|
|
@@ -1573,14 +1899,15 @@ Instantiates a fetch state with partitions for initial addresses
|
|
|
1573
1899
|
let make = (
|
|
1574
1900
|
~startBlock,
|
|
1575
1901
|
~endBlock,
|
|
1576
|
-
~
|
|
1902
|
+
~onEventRegistrations: array<Internal.onEventRegistration>,
|
|
1903
|
+
~contractConfigs: dict<IndexingAddresses.contractConfig>,
|
|
1577
1904
|
~addresses: array<Internal.indexingAddress>,
|
|
1578
1905
|
~maxAddrInPartition,
|
|
1579
1906
|
~chainId,
|
|
1580
1907
|
~maxOnBlockBufferSize,
|
|
1581
1908
|
~knownHeight,
|
|
1582
1909
|
~progressBlockNumber=startBlock - 1,
|
|
1583
|
-
~
|
|
1910
|
+
~onBlockRegistrations=[],
|
|
1584
1911
|
~blockLag=0,
|
|
1585
1912
|
~firstEventBlock=None,
|
|
1586
1913
|
): t => {
|
|
@@ -1590,38 +1917,15 @@ let make = (
|
|
|
1590
1917
|
}
|
|
1591
1918
|
|
|
1592
1919
|
let notDependingOnAddresses = []
|
|
1593
|
-
let
|
|
1920
|
+
let normalRegistrations = []
|
|
1594
1921
|
let contractNamesWithNormalEvents = Utils.Set.make()
|
|
1595
|
-
let indexingAddresses = Dict.make()
|
|
1596
|
-
let contractConfigs: dict<contractConfig> = Dict.make()
|
|
1597
|
-
|
|
1598
|
-
eventConfigs->Array.forEach(ec => {
|
|
1599
|
-
switch contractConfigs->Utils.Dict.dangerouslyGetNonOption(ec.contractName) {
|
|
1600
|
-
| Some({startBlock}) =>
|
|
1601
|
-
contractConfigs->Dict.set(
|
|
1602
|
-
ec.contractName,
|
|
1603
|
-
{
|
|
1604
|
-
startBlock: switch (startBlock, ec.startBlock) {
|
|
1605
|
-
| (Some(a), Some(b)) => Some(Pervasives.min(a, b))
|
|
1606
|
-
| (Some(_) as s, None) | (None, Some(_) as s) => s
|
|
1607
|
-
| (None, None) => None
|
|
1608
|
-
},
|
|
1609
|
-
},
|
|
1610
|
-
)
|
|
1611
|
-
| None =>
|
|
1612
|
-
contractConfigs->Dict.set(
|
|
1613
|
-
ec.contractName,
|
|
1614
|
-
{
|
|
1615
|
-
startBlock: ec.startBlock,
|
|
1616
|
-
},
|
|
1617
|
-
)
|
|
1618
|
-
}
|
|
1619
1922
|
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1923
|
+
onEventRegistrations->Array.forEach(reg => {
|
|
1924
|
+
if reg.dependsOnAddresses {
|
|
1925
|
+
normalRegistrations->Array.push(reg)
|
|
1926
|
+
contractNamesWithNormalEvents->Utils.Set.add(reg.eventConfig.contractName)->ignore
|
|
1623
1927
|
} else {
|
|
1624
|
-
notDependingOnAddresses->Array.push(
|
|
1928
|
+
notDependingOnAddresses->Array.push(reg)
|
|
1625
1929
|
}
|
|
1626
1930
|
})
|
|
1627
1931
|
|
|
@@ -1633,7 +1937,7 @@ let make = (
|
|
|
1633
1937
|
latestFetchedBlock,
|
|
1634
1938
|
selection: {
|
|
1635
1939
|
dependsOnAddresses: false,
|
|
1636
|
-
|
|
1940
|
+
onEventRegistrations: notDependingOnAddresses,
|
|
1637
1941
|
},
|
|
1638
1942
|
addressesByContractName: Dict.make(),
|
|
1639
1943
|
mergeBlock: None,
|
|
@@ -1641,13 +1945,14 @@ let make = (
|
|
|
1641
1945
|
mutPendingQueries: [],
|
|
1642
1946
|
prevQueryRange: 0,
|
|
1643
1947
|
prevPrevQueryRange: 0,
|
|
1948
|
+
prevRangeSize: 0,
|
|
1644
1949
|
latestBlockRangeUpdateBlock: 0,
|
|
1645
1950
|
})
|
|
1646
1951
|
}
|
|
1647
1952
|
|
|
1648
1953
|
let normalSelection = {
|
|
1649
1954
|
dependsOnAddresses: true,
|
|
1650
|
-
|
|
1955
|
+
onEventRegistrations: normalRegistrations,
|
|
1651
1956
|
}
|
|
1652
1957
|
|
|
1653
1958
|
let registeringContractsByContract: dict<dict<indexingAddress>> = Dict.make()
|
|
@@ -1655,32 +1960,16 @@ let make = (
|
|
|
1655
1960
|
|
|
1656
1961
|
addresses->Array.forEach(contract => {
|
|
1657
1962
|
let contractName = contract.contractName
|
|
1658
|
-
let contractStartBlock = switch contractConfigs->Utils.Dict.dangerouslyGetNonOption(
|
|
1659
|
-
contractName,
|
|
1660
|
-
) {
|
|
1661
|
-
| Some({startBlock}) => startBlock
|
|
1662
|
-
| None => None
|
|
1663
|
-
}
|
|
1664
|
-
let ia: indexingAddress = {
|
|
1665
|
-
address: contract.address,
|
|
1666
|
-
contractName: contract.contractName,
|
|
1667
|
-
registrationBlock: contract.registrationBlock,
|
|
1668
|
-
effectiveStartBlock: deriveEffectiveStartBlock(
|
|
1669
|
-
~registrationBlock=contract.registrationBlock,
|
|
1670
|
-
~contractStartBlock,
|
|
1671
|
-
),
|
|
1672
|
-
}
|
|
1673
|
-
// Track the address on fetchState regardless of whether it currently has
|
|
1674
|
-
// matching events. This way, if the config is updated later to add events
|
|
1675
|
-
// for this contract, the address is already known.
|
|
1676
|
-
indexingAddresses->Dict.set(contract.address->Address.toString, ia)
|
|
1677
1963
|
|
|
1678
1964
|
// Only addresses whose contract has events that depend on addresses get
|
|
1679
1965
|
// registered for active fetching via partitions.
|
|
1680
1966
|
if contractNamesWithNormalEvents->Utils.Set.has(contractName) {
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1967
|
+
registeringContractsByContract
|
|
1968
|
+
->Utils.Dict.getOrInsertEmptyDict(contractName)
|
|
1969
|
+
->Dict.set(
|
|
1970
|
+
contract.address->Address.toString,
|
|
1971
|
+
IndexingAddresses.makeIndexingAddress(~contract, ~contractConfigs),
|
|
1972
|
+
)
|
|
1684
1973
|
|
|
1685
1974
|
// Detect dynamic contracts by registrationBlock
|
|
1686
1975
|
if contract.registrationBlock !== -1 {
|
|
@@ -1699,12 +1988,15 @@ let make = (
|
|
|
1699
1988
|
~progressBlockNumber,
|
|
1700
1989
|
)
|
|
1701
1990
|
|
|
1702
|
-
if
|
|
1991
|
+
if (
|
|
1992
|
+
optimizedPartitions->OptimizedPartitions.count === 0 &&
|
|
1993
|
+
onBlockRegistrations->Utils.Array.isEmpty
|
|
1994
|
+
) {
|
|
1703
1995
|
JsError.throwWithMessage(
|
|
1704
1996
|
`Invalid configuration: Nothing to fetch on chain ${chainId->Int.toString}. ` ++
|
|
1705
1997
|
`addresses=${addresses->Array.length->Int.toString}, ` ++
|
|
1706
|
-
`
|
|
1707
|
-
`
|
|
1998
|
+
`onEventRegistrations=${onEventRegistrations->Array.length->Int.toString}, ` ++
|
|
1999
|
+
`normalRegistrations=${normalRegistrations
|
|
1708
2000
|
->Array.length
|
|
1709
2001
|
->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.`,
|
|
1710
2002
|
)
|
|
@@ -1715,14 +2007,14 @@ let make = (
|
|
|
1715
2007
|
// fetching, so without seeding the buffer here getNextQuery would return
|
|
1716
2008
|
// NothingToQuery and the indexer would get stuck.
|
|
1717
2009
|
let buffer = []
|
|
1718
|
-
let latestOnBlockBlockNumber = if knownHeight > 0 &&
|
|
2010
|
+
let latestOnBlockBlockNumber = if knownHeight > 0 && onBlockRegistrations->Utils.Array.notEmpty {
|
|
1719
2011
|
let maxBlockNumber = switch optimizedPartitions->OptimizedPartitions.getLatestFullyFetchedBlock {
|
|
1720
2012
|
| None => knownHeight
|
|
1721
2013
|
| Some(latestFullyFetchedBlock) => latestFullyFetchedBlock.blockNumber
|
|
1722
2014
|
}
|
|
1723
2015
|
appendOnBlockItems(
|
|
1724
2016
|
~mutItems=buffer,
|
|
1725
|
-
~
|
|
2017
|
+
~onBlockRegistrations,
|
|
1726
2018
|
~indexerStartBlock=startBlock,
|
|
1727
2019
|
~fromBlock=progressBlockNumber,
|
|
1728
2020
|
~maxBlockNumber,
|
|
@@ -1740,17 +2032,14 @@ let make = (
|
|
|
1740
2032
|
endBlock,
|
|
1741
2033
|
latestOnBlockBlockNumber,
|
|
1742
2034
|
normalSelection,
|
|
1743
|
-
indexingAddresses,
|
|
1744
2035
|
blockLag,
|
|
1745
|
-
|
|
2036
|
+
onBlockRegistrations,
|
|
1746
2037
|
maxOnBlockBufferSize,
|
|
1747
2038
|
knownHeight,
|
|
1748
2039
|
buffer,
|
|
1749
2040
|
firstEventBlock,
|
|
1750
2041
|
}
|
|
1751
2042
|
|
|
1752
|
-
let numAddresses = indexingAddresses->Utils.Dict.size
|
|
1753
|
-
Prometheus.IndexingAddresses.set(~addressesCount=numAddresses, ~chainId)
|
|
1754
2043
|
Prometheus.IndexingPartitions.set(
|
|
1755
2044
|
~partitionsCount=optimizedPartitions->OptimizedPartitions.count,
|
|
1756
2045
|
~chainId,
|
|
@@ -1767,25 +2056,6 @@ let make = (
|
|
|
1767
2056
|
|
|
1768
2057
|
let bufferSize = ({buffer}: t) => buffer->Array.length
|
|
1769
2058
|
|
|
1770
|
-
// Number of buffered items at or below the ready frontier (processable now,
|
|
1771
|
-
// i.e. not stuck behind a gap from a lagging partition or out-of-order chunk).
|
|
1772
|
-
// The buffer is kept sorted, so binary-search the frontier in O(log n).
|
|
1773
|
-
let bufferReadyCount = (fetchState: t) => {
|
|
1774
|
-
let frontier = fetchState->bufferBlockNumber
|
|
1775
|
-
let buffer = fetchState.buffer
|
|
1776
|
-
let lo = ref(0)
|
|
1777
|
-
let hi = ref(buffer->Array.length)
|
|
1778
|
-
while lo.contents < hi.contents {
|
|
1779
|
-
let mid = (lo.contents + hi.contents) / 2
|
|
1780
|
-
if buffer->Array.getUnsafe(mid)->Internal.getItemBlockNumber <= frontier {
|
|
1781
|
-
lo := mid + 1
|
|
1782
|
-
} else {
|
|
1783
|
-
hi := mid
|
|
1784
|
-
}
|
|
1785
|
-
}
|
|
1786
|
-
lo.contents
|
|
1787
|
-
}
|
|
1788
|
-
|
|
1789
2059
|
let rollbackPendingQueries = (mutPendingQueries: array<pendingQuery>, ~targetBlockNumber) => {
|
|
1790
2060
|
// - Remove queries where fromBlock > target
|
|
1791
2061
|
// - Cap fetchedBlock at target where fetchedBlock > target
|
|
@@ -1817,18 +2087,11 @@ Always recreates optimized partitions to avoid duplicate addresses:
|
|
|
1817
2087
|
- Non-wildcard with lfb <= target: keep, adjust pending queries and mergeBlock
|
|
1818
2088
|
- Non-wildcard with lfb > target: delete, track addresses for recreation
|
|
1819
2089
|
*/
|
|
1820
|
-
let rollback = (fetchState: t, ~targetBlockNumber) => {
|
|
1821
|
-
// Step 1:
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
fetchState.indexingAddresses->Utils.Dict.forEachWithKey((indexingContract, address) => {
|
|
1826
|
-
if indexingContract.registrationBlock > targetBlockNumber {
|
|
1827
|
-
let _ = addressesToRemove->Utils.Set.add(address->Address.unsafeFromString)
|
|
1828
|
-
} else {
|
|
1829
|
-
indexingAddresses->Dict.set(address, indexingContract)
|
|
1830
|
-
}
|
|
1831
|
-
})
|
|
2090
|
+
let rollback = (fetchState: t, ~indexingAddresses: IndexingAddresses.t, ~targetBlockNumber) => {
|
|
2091
|
+
// Step 1: Prune addresses registered after the target block. The pruned index is
|
|
2092
|
+
// then the source of truth for partition cleanup below — an address survives iff
|
|
2093
|
+
// it's still in the index.
|
|
2094
|
+
indexingAddresses->IndexingAddresses.rollbackInPlace(~targetBlockNumber)
|
|
1832
2095
|
|
|
1833
2096
|
// Step 2: Categorize partitions
|
|
1834
2097
|
let keptPartitions = []
|
|
@@ -1858,18 +2121,12 @@ let rollback = (fetchState: t, ~targetBlockNumber) => {
|
|
|
1858
2121
|
| _ if p.latestFetchedBlock.blockNumber > targetBlockNumber =>
|
|
1859
2122
|
p.addressesByContractName->Utils.Dict.forEachWithKey((addresses, contractName) => {
|
|
1860
2123
|
addresses->Array.forEach(address => {
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
indexingAddresses
|
|
1864
|
-
->Utils.Dict.dangerouslyGetNonOption(address->Address.toString)
|
|
1865
|
-
->Option.isSome
|
|
1866
|
-
) {
|
|
2124
|
+
switch indexingAddresses->IndexingAddresses.get(address->Address.toString) {
|
|
2125
|
+
| Some(indexingContract) =>
|
|
1867
2126
|
let registeringContracts =
|
|
1868
2127
|
registeringContractsByContract->Utils.Dict.getOrInsertEmptyDict(contractName)
|
|
1869
|
-
registeringContracts->Dict.set(
|
|
1870
|
-
|
|
1871
|
-
indexingAddresses->Dict.getUnsafe(address->Address.toString),
|
|
1872
|
-
)
|
|
2128
|
+
registeringContracts->Dict.set(address->Address.toString, indexingContract)
|
|
2129
|
+
| None => ()
|
|
1873
2130
|
}
|
|
1874
2131
|
})
|
|
1875
2132
|
})
|
|
@@ -1882,11 +2139,13 @@ let rollback = (fetchState: t, ~targetBlockNumber) => {
|
|
|
1882
2139
|
| other => other
|
|
1883
2140
|
}
|
|
1884
2141
|
|
|
1885
|
-
//
|
|
2142
|
+
// Drop addresses pruned from the index
|
|
1886
2143
|
let rollbackedAddressesByContractName = Dict.make()
|
|
1887
2144
|
addressesByContractName->Utils.Dict.forEachWithKey((addresses, contractName) => {
|
|
1888
2145
|
let keptAddresses =
|
|
1889
|
-
addresses->Array.filter(address =>
|
|
2146
|
+
addresses->Array.filter(address =>
|
|
2147
|
+
indexingAddresses->IndexingAddresses.get(address->Address.toString)->Option.isSome
|
|
2148
|
+
)
|
|
1890
2149
|
if keptAddresses->Array.length > 0 {
|
|
1891
2150
|
rollbackedAddressesByContractName->Dict.set(contractName, keptAddresses)
|
|
1892
2151
|
}
|
|
@@ -1930,7 +2189,6 @@ let rollback = (fetchState: t, ~targetBlockNumber) => {
|
|
|
1930
2189
|
),
|
|
1931
2190
|
}->updateInternal(
|
|
1932
2191
|
~optimizedPartitions,
|
|
1933
|
-
~indexingAddresses,
|
|
1934
2192
|
~mutItems=fetchState.buffer->Array.filter(item =>
|
|
1935
2193
|
switch item {
|
|
1936
2194
|
| Event({blockNumber})
|