envio 3.3.0-alpha.1 → 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 +43 -9
- package/src/ChainFetching.res.mjs +34 -15
- package/src/ChainMetadata.res +1 -11
- package/src/ChainMetadata.res.mjs +1 -10
- package/src/ChainState.res +605 -151
- package/src/ChainState.res.mjs +427 -104
- package/src/ChainState.resi +72 -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 +113 -89
- package/src/CrossChainState.res.mjs +64 -60
- package/src/CrossChainState.resi +1 -1
- package/src/Ecosystem.res +5 -6
- package/src/Ecosystem.res.mjs +3 -3
- 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 +544 -356
- package/src/FetchState.res.mjs +412 -471
- 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/IndexerState.res +10 -3
- package/src/IndexerState.res.mjs +11 -4
- package/src/IndexerState.resi +2 -1
- 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 -63
- package/src/Prometheus.res.mjs +124 -171
- 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 +77 -19
- package/src/sources/SourceManager.res.mjs +63 -21
- package/src/sources/SourceManager.resi +10 -0
- 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,15 +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,
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
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,
|
|
25
25
|
// Stores latestFetchedBlock when query completes. Only needed to persist
|
|
26
26
|
// timestamp while earlier queries are still pending before updating
|
|
27
27
|
// the partition's latestFetchedBlock.
|
|
@@ -65,38 +65,51 @@ type query = {
|
|
|
65
65
|
fromBlock: int,
|
|
66
66
|
toBlock: option<int>,
|
|
67
67
|
isChunk: bool,
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
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,
|
|
77
77
|
selection: selection,
|
|
78
78
|
addressesByContractName: dict<array<Address.t>>,
|
|
79
|
-
indexingAddresses: dict<indexingAddress>,
|
|
80
79
|
}
|
|
81
80
|
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
+
)
|
|
112
|
+
}
|
|
100
113
|
|
|
101
114
|
// Calculate the chunk range from history using min-of-last-3-ranges heuristic
|
|
102
115
|
let getMinHistoryRange = (p: partition) => {
|
|
@@ -106,6 +119,15 @@ let getMinHistoryRange = (p: partition) => {
|
|
|
106
119
|
}
|
|
107
120
|
}
|
|
108
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
|
+
|
|
109
131
|
let getMinQueryRange = (partitions: array<partition>) => {
|
|
110
132
|
let min = ref(0)
|
|
111
133
|
for i in 0 to partitions->Array.length - 1 {
|
|
@@ -185,6 +207,12 @@ module OptimizedPartitions = {
|
|
|
185
207
|
let newId = nextPartitionIndexRef.contents->Int.toString
|
|
186
208
|
nextPartitionIndexRef := nextPartitionIndexRef.contents + 1
|
|
187
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.)
|
|
188
216
|
{
|
|
189
217
|
id: newId,
|
|
190
218
|
dynamicContract: Some(contractName),
|
|
@@ -195,7 +223,7 @@ module OptimizedPartitions = {
|
|
|
195
223
|
mutPendingQueries: [],
|
|
196
224
|
prevQueryRange: minRange,
|
|
197
225
|
prevPrevQueryRange: minRange,
|
|
198
|
-
prevRangeSize:
|
|
226
|
+
prevRangeSize: (inheritedDensity *. minRange->Int.toFloat)->Math.ceil->Float.toInt,
|
|
199
227
|
latestBlockRangeUpdateBlock: 0,
|
|
200
228
|
}
|
|
201
229
|
}
|
|
@@ -453,15 +481,21 @@ module OptimizedPartitions = {
|
|
|
453
481
|
switch query.toBlock {
|
|
454
482
|
| None => latestFetchedBlock.blockNumber < knownHeight - 10 // Don't update block range when very close to the head
|
|
455
483
|
| Some(queryToBlock) =>
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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.
|
|
461
494
|
switch getMinHistoryRange(p) {
|
|
462
495
|
| None => false // Chunking not active yet, don't update
|
|
463
496
|
| Some(minHistoryRange) => queryToBlock - query.fromBlock + 1 >= minHistoryRange
|
|
464
497
|
}
|
|
498
|
+
}
|
|
465
499
|
}
|
|
466
500
|
let updatedPrevQueryRange = shouldUpdateBlockRange ? blockRange : p.prevQueryRange
|
|
467
501
|
let updatedPrevPrevQueryRange = shouldUpdateBlockRange ? p.prevQueryRange : p.prevPrevQueryRange
|
|
@@ -519,10 +553,8 @@ type t = {
|
|
|
519
553
|
startBlock: int,
|
|
520
554
|
endBlock: option<int>,
|
|
521
555
|
normalSelection: selection,
|
|
522
|
-
// By address
|
|
523
|
-
indexingAddresses: dict<indexingAddress>,
|
|
524
556
|
// By contract name
|
|
525
|
-
contractConfigs: dict<contractConfig>,
|
|
557
|
+
contractConfigs: dict<IndexingAddresses.contractConfig>,
|
|
526
558
|
// Not used for logic - only metadata
|
|
527
559
|
chainId: int,
|
|
528
560
|
// The block number of the latest block which was added to the queue
|
|
@@ -536,10 +568,10 @@ type t = {
|
|
|
536
568
|
// Buffer of items ordered from earliest to latest
|
|
537
569
|
buffer: array<Internal.item>,
|
|
538
570
|
// Caps how far ahead onBlock items are pre-generated (set to 2x the batch
|
|
539
|
-
// size).
|
|
540
|
-
// chain
|
|
571
|
+
// size). Event fetch depth is bounded separately, by CrossChainState's
|
|
572
|
+
// cross-chain admission against the indexer-wide buffer pool.
|
|
541
573
|
maxOnBlockBufferSize: int,
|
|
542
|
-
|
|
574
|
+
onBlockRegistrations: array<Internal.onBlockRegistration>,
|
|
543
575
|
knownHeight: int,
|
|
544
576
|
firstEventBlock: option<int>,
|
|
545
577
|
}
|
|
@@ -575,6 +607,25 @@ let bufferBlock = ({optimizedPartitions, latestOnBlockBlockNumber}: t) => {
|
|
|
575
607
|
}
|
|
576
608
|
}
|
|
577
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
|
+
|
|
578
629
|
/*
|
|
579
630
|
Comparitor for two events from the same chain. No need for chain id or timestamp
|
|
580
631
|
*/
|
|
@@ -590,15 +641,13 @@ let compareBufferItem = (a: Internal.item, b: Internal.item) => {
|
|
|
590
641
|
// Some big number which should be bigger than any log index
|
|
591
642
|
let blockItemLogIndex = 16777216
|
|
592
643
|
|
|
593
|
-
let numAddresses = fetchState => fetchState.indexingAddresses->Utils.Dict.size
|
|
594
|
-
|
|
595
644
|
// Appends Block items produced by the onBlock handlers for every block in
|
|
596
645
|
// (fromBlock, maxBlockNumber] into mutItems and returns the new
|
|
597
646
|
// latestOnBlockBlockNumber pointer. maxOnBlockBufferSize bounds how many items
|
|
598
647
|
// are generated at once to prevent OOM.
|
|
599
648
|
let appendOnBlockItems = (
|
|
600
649
|
~mutItems: array<Internal.item>,
|
|
601
|
-
~
|
|
650
|
+
~onBlockRegistrations: array<Internal.onBlockRegistration>,
|
|
602
651
|
~indexerStartBlock,
|
|
603
652
|
~fromBlock,
|
|
604
653
|
~maxBlockNumber,
|
|
@@ -618,27 +667,27 @@ let appendOnBlockItems = (
|
|
|
618
667
|
let blockNumber = latestOnBlockBlockNumber.contents + 1
|
|
619
668
|
latestOnBlockBlockNumber := blockNumber
|
|
620
669
|
|
|
621
|
-
for configIdx in 0 to
|
|
622
|
-
let
|
|
670
|
+
for configIdx in 0 to onBlockRegistrations->Array.length - 1 {
|
|
671
|
+
let onBlockRegistration = onBlockRegistrations->Array.getUnsafe(configIdx)
|
|
623
672
|
|
|
624
|
-
let handlerStartBlock = switch
|
|
673
|
+
let handlerStartBlock = switch onBlockRegistration.startBlock {
|
|
625
674
|
| Some(startBlock) => startBlock
|
|
626
675
|
| None => indexerStartBlock
|
|
627
676
|
}
|
|
628
677
|
|
|
629
678
|
if (
|
|
630
679
|
blockNumber >= handlerStartBlock &&
|
|
631
|
-
switch
|
|
680
|
+
switch onBlockRegistration.endBlock {
|
|
632
681
|
| Some(endBlock) => blockNumber <= endBlock
|
|
633
682
|
| None => true
|
|
634
683
|
} &&
|
|
635
|
-
(blockNumber - handlerStartBlock)->Pervasives.mod(
|
|
684
|
+
(blockNumber - handlerStartBlock)->Pervasives.mod(onBlockRegistration.interval) === 0
|
|
636
685
|
) {
|
|
637
686
|
mutItems->Array.push(
|
|
638
687
|
Block({
|
|
639
|
-
|
|
688
|
+
onBlockRegistration,
|
|
640
689
|
blockNumber,
|
|
641
|
-
logIndex: blockItemLogIndex +
|
|
690
|
+
logIndex: blockItemLogIndex + onBlockRegistration.index,
|
|
642
691
|
}),
|
|
643
692
|
)
|
|
644
693
|
newItemsCounter := newItemsCounter.contents + 1
|
|
@@ -656,16 +705,15 @@ Runs partition optimization when partitions change.
|
|
|
656
705
|
let updateInternal = (
|
|
657
706
|
fetchState: t,
|
|
658
707
|
~optimizedPartitions=fetchState.optimizedPartitions,
|
|
659
|
-
~indexingAddresses=fetchState.indexingAddresses,
|
|
660
708
|
~mutItems=?,
|
|
661
709
|
~blockLag=fetchState.blockLag,
|
|
662
710
|
~knownHeight=fetchState.knownHeight,
|
|
663
711
|
): t => {
|
|
664
712
|
let mutItemsRef = ref(mutItems)
|
|
665
713
|
|
|
666
|
-
let latestOnBlockBlockNumber = switch fetchState.
|
|
714
|
+
let latestOnBlockBlockNumber = switch fetchState.onBlockRegistrations {
|
|
667
715
|
| [] => knownHeight
|
|
668
|
-
|
|
|
716
|
+
| onBlockRegistrations => {
|
|
669
717
|
// Calculate the max block number we are going to create items for
|
|
670
718
|
// Use maxOnBlockBufferSize to get the last target item in the buffer
|
|
671
719
|
//
|
|
@@ -693,7 +741,7 @@ let updateInternal = (
|
|
|
693
741
|
|
|
694
742
|
appendOnBlockItems(
|
|
695
743
|
~mutItems,
|
|
696
|
-
~
|
|
744
|
+
~onBlockRegistrations,
|
|
697
745
|
~indexerStartBlock=fetchState.startBlock,
|
|
698
746
|
~fromBlock=fetchState.latestOnBlockBlockNumber,
|
|
699
747
|
~maxBlockNumber,
|
|
@@ -708,11 +756,10 @@ let updateInternal = (
|
|
|
708
756
|
contractConfigs: fetchState.contractConfigs,
|
|
709
757
|
normalSelection: fetchState.normalSelection,
|
|
710
758
|
chainId: fetchState.chainId,
|
|
711
|
-
|
|
759
|
+
onBlockRegistrations: fetchState.onBlockRegistrations,
|
|
712
760
|
maxOnBlockBufferSize: fetchState.maxOnBlockBufferSize,
|
|
713
761
|
optimizedPartitions,
|
|
714
762
|
latestOnBlockBlockNumber,
|
|
715
|
-
indexingAddresses,
|
|
716
763
|
blockLag,
|
|
717
764
|
knownHeight,
|
|
718
765
|
buffer: switch mutItemsRef.contents {
|
|
@@ -740,12 +787,6 @@ let updateInternal = (
|
|
|
740
787
|
~blockNumber=updatedFetchState->bufferBlockNumber,
|
|
741
788
|
~chainId=fetchState.chainId,
|
|
742
789
|
)
|
|
743
|
-
if indexingAddresses !== fetchState.indexingAddresses {
|
|
744
|
-
Prometheus.IndexingAddresses.set(
|
|
745
|
-
~addressesCount=updatedFetchState->numAddresses,
|
|
746
|
-
~chainId=fetchState.chainId,
|
|
747
|
-
)
|
|
748
|
-
}
|
|
749
790
|
|
|
750
791
|
updatedFetchState
|
|
751
792
|
}
|
|
@@ -976,22 +1017,22 @@ OptimizedPartitions.t => {
|
|
|
976
1017
|
|
|
977
1018
|
let registerDynamicContracts = (
|
|
978
1019
|
fetchState: t,
|
|
1020
|
+
~indexingAddresses: IndexingAddresses.t,
|
|
979
1021
|
// These are raw items which might have dynamic contracts received from contractRegister call.
|
|
980
1022
|
// Might contain duplicates which we should filter out
|
|
981
1023
|
items: array<Internal.item>,
|
|
982
1024
|
) => {
|
|
983
|
-
if fetchState.normalSelection.
|
|
1025
|
+
if fetchState.normalSelection.onEventRegistrations->Utils.Array.isEmpty {
|
|
984
1026
|
// Can the normalSelection be empty?
|
|
985
1027
|
JsError.throwWithMessage(
|
|
986
1028
|
"Invalid configuration. No events to fetch for the dynamic contract registration.",
|
|
987
1029
|
)
|
|
988
1030
|
}
|
|
989
1031
|
|
|
990
|
-
let indexingAddresses = fetchState.indexingAddresses
|
|
991
1032
|
let registeringContractsByContract: dict<dict<indexingAddress>> = Dict.make()
|
|
992
1033
|
let earliestRegisteringEventBlockNumber = ref(%raw(`Infinity`))
|
|
993
1034
|
// Addresses registered for contracts without matching events. These are not
|
|
994
|
-
// added to partitions, but they are tracked on
|
|
1035
|
+
// added to partitions, but they are tracked on indexingAddresses
|
|
995
1036
|
// so that later conflicting registrations are detected, and are persisted
|
|
996
1037
|
// to envio_addresses so they can be picked up on restart with updated config.
|
|
997
1038
|
let noEventsAddresses: dict<indexingAddress> = Dict.make()
|
|
@@ -1017,15 +1058,13 @@ let registerDynamicContracts = (
|
|
|
1017
1058
|
address: dc.address,
|
|
1018
1059
|
contractName: dc.contractName,
|
|
1019
1060
|
registrationBlock: dc.registrationBlock,
|
|
1020
|
-
effectiveStartBlock: deriveEffectiveStartBlock(
|
|
1061
|
+
effectiveStartBlock: IndexingAddresses.deriveEffectiveStartBlock(
|
|
1021
1062
|
~registrationBlock=dc.registrationBlock,
|
|
1022
1063
|
~contractStartBlock,
|
|
1023
1064
|
),
|
|
1024
1065
|
}
|
|
1025
1066
|
// Prevent registering already indexing contracts
|
|
1026
|
-
switch indexingAddresses->
|
|
1027
|
-
dc.address->Address.toString,
|
|
1028
|
-
) {
|
|
1067
|
+
switch indexingAddresses->IndexingAddresses.get(dc.address->Address.toString) {
|
|
1029
1068
|
| Some(existingContract) =>
|
|
1030
1069
|
// FIXME: Instead of filtering out duplicates,
|
|
1031
1070
|
// we should check the block number first.
|
|
@@ -1080,7 +1119,7 @@ let registerDynamicContracts = (
|
|
|
1080
1119
|
address: dc.address,
|
|
1081
1120
|
contractName: dc.contractName,
|
|
1082
1121
|
registrationBlock: dc.registrationBlock,
|
|
1083
|
-
effectiveStartBlock: deriveEffectiveStartBlock(
|
|
1122
|
+
effectiveStartBlock: IndexingAddresses.deriveEffectiveStartBlock(
|
|
1084
1123
|
~registrationBlock=dc.registrationBlock,
|
|
1085
1124
|
~contractStartBlock=None,
|
|
1086
1125
|
),
|
|
@@ -1088,9 +1127,7 @@ let registerDynamicContracts = (
|
|
|
1088
1127
|
// Prevent duplicate logging/persistence when the same address is
|
|
1089
1128
|
// already tracked on fetchState, either from the db on startup or
|
|
1090
1129
|
// from an earlier registration in this batch.
|
|
1091
|
-
switch indexingAddresses->
|
|
1092
|
-
dc.address->Address.toString,
|
|
1093
|
-
) {
|
|
1130
|
+
switch indexingAddresses->IndexingAddresses.get(dc.address->Address.toString) {
|
|
1094
1131
|
| Some(existingContract) =>
|
|
1095
1132
|
if existingContract.contractName != dc.contractName {
|
|
1096
1133
|
fetchState->warnDifferentContractType(~existingContract, ~dc=dcAsIndexingAddress)
|
|
@@ -1144,12 +1181,10 @@ let registerDynamicContracts = (
|
|
|
1144
1181
|
// Only dcs for contracts without events. Track them on
|
|
1145
1182
|
// indexingAddresses so subsequent registrations see them, but don't touch
|
|
1146
1183
|
// partitions since there's nothing to fetch for them.
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
fetchState->updateInternal(~indexingAddresses=newIndexingContracts)
|
|
1184
|
+
indexingAddresses->IndexingAddresses.register(noEventsAddresses)
|
|
1185
|
+
fetchState
|
|
1150
1186
|
| (_, _) => {
|
|
1151
1187
|
let newPartitions = []
|
|
1152
|
-
let newIndexingAddresses = indexingAddresses->Utils.Dict.shallowCopy
|
|
1153
1188
|
let dynamicContractsRef = ref(fetchState.optimizedPartitions.dynamicContracts)
|
|
1154
1189
|
let mutExistingPartitions = fetchState.optimizedPartitions.entities->Dict.valuesToArray
|
|
1155
1190
|
|
|
@@ -1231,10 +1266,10 @@ let registerDynamicContracts = (
|
|
|
1231
1266
|
}
|
|
1232
1267
|
|
|
1233
1268
|
let registeringContracts = registeringContractsByContract->Dict.getUnsafe(contractName)
|
|
1234
|
-
|
|
1269
|
+
indexingAddresses->IndexingAddresses.register(registeringContracts)
|
|
1235
1270
|
}
|
|
1236
1271
|
// Include no-events dcs so later batches detect conflicts against them.
|
|
1237
|
-
|
|
1272
|
+
indexingAddresses->IndexingAddresses.register(noEventsAddresses)
|
|
1238
1273
|
|
|
1239
1274
|
let optimizedPartitions = createPartitionsFromIndexingAddresses(
|
|
1240
1275
|
~registeringContractsByContract,
|
|
@@ -1247,7 +1282,7 @@ let registerDynamicContracts = (
|
|
|
1247
1282
|
~progressBlockNumber=0,
|
|
1248
1283
|
)
|
|
1249
1284
|
|
|
1250
|
-
fetchState->updateInternal(~optimizedPartitions
|
|
1285
|
+
fetchState->updateInternal(~optimizedPartitions)
|
|
1251
1286
|
}
|
|
1252
1287
|
}
|
|
1253
1288
|
}
|
|
@@ -1260,6 +1295,7 @@ newItems are ordered earliest to latest (as they are returned from the worker)
|
|
|
1260
1295
|
*/
|
|
1261
1296
|
let handleQueryResult = (
|
|
1262
1297
|
fetchState: t,
|
|
1298
|
+
~indexingAddresses: IndexingAddresses.t,
|
|
1263
1299
|
~query: query,
|
|
1264
1300
|
~latestFetchedBlock: blockNumberAndTimestamp,
|
|
1265
1301
|
~newItems,
|
|
@@ -1270,9 +1306,10 @@ let handleQueryResult = (
|
|
|
1270
1306
|
// param-level analogue of EventRouter's srcAddress effectiveStartBlock check.
|
|
1271
1307
|
let newItems = newItems->Array.filter(item =>
|
|
1272
1308
|
switch item {
|
|
1273
|
-
| Internal.Event({
|
|
1274
|
-
switch
|
|
1275
|
-
| Some(filter) =>
|
|
1309
|
+
| Internal.Event({onEventRegistration, payload, blockNumber}) =>
|
|
1310
|
+
switch onEventRegistration.clientAddressFilter {
|
|
1311
|
+
| Some(filter) =>
|
|
1312
|
+
filter(payload, blockNumber, indexingAddresses->IndexingAddresses.rawForFilter)
|
|
1276
1313
|
| None => true
|
|
1277
1314
|
}
|
|
1278
1315
|
| _ => true
|
|
@@ -1308,7 +1345,8 @@ let startFetchingQueries = ({optimizedPartitions}: t, ~queries: array<query>) =>
|
|
|
1308
1345
|
fromBlock: q.fromBlock,
|
|
1309
1346
|
toBlock: q.toBlock,
|
|
1310
1347
|
isChunk: q.isChunk,
|
|
1311
|
-
|
|
1348
|
+
itemsTarget: q.itemsTarget,
|
|
1349
|
+
itemsEst: q.itemsEst,
|
|
1312
1350
|
fetchedBlock: None,
|
|
1313
1351
|
}
|
|
1314
1352
|
|
|
@@ -1329,163 +1367,218 @@ let startFetchingQueries = ({optimizedPartitions}: t, ~queries: array<query>) =>
|
|
|
1329
1367
|
}
|
|
1330
1368
|
}
|
|
1331
1369
|
|
|
1332
|
-
|
|
1333
|
-
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 = (
|
|
1334
1382
|
queries: array<query>,
|
|
1335
1383
|
~partitionId: string,
|
|
1336
1384
|
~rangeFromBlock: int,
|
|
1337
1385
|
~rangeEndBlock: option<int>,
|
|
1338
|
-
~
|
|
1386
|
+
~knownHeight: int,
|
|
1387
|
+
~chainTargetBlock: int,
|
|
1339
1388
|
~maybeChunkRange: option<int>,
|
|
1389
|
+
~maxChunks: int,
|
|
1340
1390
|
~partition: partition,
|
|
1391
|
+
~partitionBudget: float,
|
|
1392
|
+
~chunkItemsMultiplier: float,
|
|
1341
1393
|
~selection: selection,
|
|
1342
1394
|
~addressesByContractName: dict<array<Address.t>>,
|
|
1343
|
-
~indexingAddresses: dict<indexingAddress>,
|
|
1344
1395
|
) => {
|
|
1345
|
-
|
|
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 {
|
|
1346
1401
|
switch rangeEndBlock {
|
|
1347
1402
|
| Some(endBlock) if rangeFromBlock > endBlock => ()
|
|
1348
1403
|
| _ =>
|
|
1349
|
-
|
|
1350
|
-
|
|
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
|
+
)
|
|
1351
1422
|
queries->Array.push({
|
|
1352
1423
|
partitionId,
|
|
1353
1424
|
fromBlock: rangeFromBlock,
|
|
1354
1425
|
toBlock: rangeEndBlock,
|
|
1355
1426
|
selection,
|
|
1356
|
-
isChunk
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
~fromBlock=rangeFromBlock,
|
|
1360
|
-
~toBlock=rangeEndBlock,
|
|
1361
|
-
~maxQueryBlockNumber,
|
|
1362
|
-
),
|
|
1363
|
-
chainId: 0,
|
|
1364
|
-
progress: 0.,
|
|
1427
|
+
isChunk,
|
|
1428
|
+
itemsTarget,
|
|
1429
|
+
itemsEst,
|
|
1365
1430
|
addressesByContractName,
|
|
1366
|
-
indexingAddresses,
|
|
1367
1431
|
})
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
}
|
|
1432
|
+
cost := cost.contents +. itemsEst->Int.toFloat
|
|
1433
|
+
}
|
|
1434
|
+
switch (trustedDensity, maybeChunkRange) {
|
|
1435
|
+
| (Some(density), Some(chunkRange)) if density > 0. =>
|
|
1373
1436
|
let chunkSize = Js.Math.ceil_int(chunkRange->Int.toFloat *. 1.8)
|
|
1374
|
-
|
|
1375
|
-
// quickly and refresh the chunking heuristic, then three full-size chunks.
|
|
1376
|
-
let probeSize = Js.Math.ceil_int(chunkRange->Int.toFloat *. 0.9)
|
|
1377
|
-
let getChunkSize = chunkIdx => chunkIdx < 2 ? probeSize : chunkSize
|
|
1378
|
-
if rangeFromBlock + getChunkSize(0) + getChunkSize(1) - 1 <= maxBlock {
|
|
1437
|
+
if rangeFromBlock + chunkSize * 2 - 1 <= maxBlock {
|
|
1379
1438
|
let chunkFromBlock = ref(rangeFromBlock)
|
|
1380
1439
|
let chunkIdx = ref(0)
|
|
1381
1440
|
while (
|
|
1382
|
-
chunkIdx.contents <
|
|
1383
|
-
chunkFromBlock.contents + getChunkSize(chunkIdx.contents) - 1 <= maxBlock
|
|
1441
|
+
chunkIdx.contents < maxChunks && chunkFromBlock.contents + chunkSize - 1 <= maxBlock
|
|
1384
1442
|
) {
|
|
1385
|
-
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
|
+
)
|
|
1386
1456
|
queries->Array.push({
|
|
1387
1457
|
partitionId,
|
|
1388
1458
|
fromBlock: chunkFromBlock.contents,
|
|
1389
1459
|
toBlock: Some(chunkToBlock),
|
|
1390
1460
|
isChunk: true,
|
|
1391
1461
|
selection,
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
~fromBlock=chunkFromBlock.contents,
|
|
1395
|
-
~toBlock=Some(chunkToBlock),
|
|
1396
|
-
~maxQueryBlockNumber,
|
|
1397
|
-
),
|
|
1398
|
-
chainId: 0,
|
|
1399
|
-
progress: 0.,
|
|
1462
|
+
itemsTarget,
|
|
1463
|
+
itemsEst,
|
|
1400
1464
|
addressesByContractName,
|
|
1401
|
-
indexingAddresses,
|
|
1402
1465
|
})
|
|
1466
|
+
cost := cost.contents +. itemsEst->Int.toFloat
|
|
1403
1467
|
chunkFromBlock := chunkToBlock + 1
|
|
1404
1468
|
chunkIdx := chunkIdx.contents + 1
|
|
1405
1469
|
}
|
|
1406
1470
|
} else {
|
|
1407
1471
|
// Not enough room for 2 chunks, fall back to a single query
|
|
1408
|
-
|
|
1409
|
-
partitionId,
|
|
1410
|
-
fromBlock: rangeFromBlock,
|
|
1411
|
-
toBlock: rangeEndBlock,
|
|
1412
|
-
selection,
|
|
1413
|
-
isChunk: rangeEndBlock !== None,
|
|
1414
|
-
estResponseSize: calculateEstResponseSize(
|
|
1415
|
-
partition,
|
|
1416
|
-
~fromBlock=rangeFromBlock,
|
|
1417
|
-
~toBlock=rangeEndBlock,
|
|
1418
|
-
~maxQueryBlockNumber,
|
|
1419
|
-
),
|
|
1420
|
-
chainId: 0,
|
|
1421
|
-
progress: 0.,
|
|
1422
|
-
addressesByContractName,
|
|
1423
|
-
indexingAddresses,
|
|
1424
|
-
})
|
|
1472
|
+
pushSingleQuery(~density, ~isChunk=rangeEndBlock !== None)
|
|
1425
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)
|
|
1426
1478
|
}
|
|
1427
1479
|
}
|
|
1428
1480
|
}
|
|
1481
|
+
cost.contents
|
|
1429
1482
|
}
|
|
1430
1483
|
|
|
1431
|
-
//
|
|
1432
|
-
|
|
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
|
+
}
|
|
1433
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
|
+
}
|
|
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.
|
|
1434
1552
|
let getNextQuery = (
|
|
1435
|
-
{
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
blockLag,
|
|
1440
|
-
latestOnBlockBlockNumber,
|
|
1441
|
-
knownHeight,
|
|
1442
|
-
} as fetchState: t,
|
|
1443
|
-
~budget,
|
|
1444
|
-
~chainPendingBudget,
|
|
1553
|
+
{optimizedPartitions, blockLag, latestOnBlockBlockNumber, knownHeight, endBlock}: t,
|
|
1554
|
+
~chainTargetBlock: int,
|
|
1555
|
+
~chainTargetItems: float,
|
|
1556
|
+
~chunkItemsMultiplier: float=1.,
|
|
1445
1557
|
) => {
|
|
1446
1558
|
let headBlockNumber = knownHeight - blockLag
|
|
1447
1559
|
if headBlockNumber <= 0 {
|
|
1448
1560
|
WaitingForNewBlock
|
|
1449
|
-
} else if budget <= 0 {
|
|
1450
|
-
// No room left in the shared buffer pool for this chain; wait for processing
|
|
1451
|
-
// to drain before fetching more.
|
|
1452
|
-
NothingToQuery
|
|
1453
1561
|
} else {
|
|
1454
1562
|
let isOnBlockBehindTheHead = latestOnBlockBlockNumber < headBlockNumber
|
|
1455
1563
|
let shouldWaitForNewBlock = ref(
|
|
1456
|
-
switch
|
|
1564
|
+
switch endBlock {
|
|
1457
1565
|
| Some(endBlock) => headBlockNumber < endBlock
|
|
1458
1566
|
| None => true
|
|
1459
1567
|
} &&
|
|
1460
1568
|
!isOnBlockBehindTheHead,
|
|
1461
1569
|
)
|
|
1462
1570
|
|
|
1463
|
-
// Limit how far ahead we fetch to budget items (plus what's already in
|
|
1464
|
-
// flight) so processing always has buffer without ballooning memory. A
|
|
1465
|
-
// partition that fetched further is skipped until the buffer drains.
|
|
1466
|
-
let maxQueryBlockNumber = {
|
|
1467
|
-
switch buffer->Array.get(budget + chainPendingBudget->Float.toInt - 1) {
|
|
1468
|
-
| Some(item) =>
|
|
1469
|
-
// Just in case check that we don't query beyond the current block
|
|
1470
|
-
Pervasives.min(item->Internal.getItemBlockNumber, knownHeight)
|
|
1471
|
-
| None => knownHeight
|
|
1472
|
-
}
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
let queries = []
|
|
1476
|
-
|
|
1477
1571
|
let partitionsCount = optimizedPartitions.idsInAscOrder->Array.length
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
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 {
|
|
1481
1577
|
let partitionId = optimizedPartitions.idsInAscOrder->Array.getUnsafe(idx)
|
|
1482
1578
|
let p = optimizedPartitions.entities->Dict.getUnsafe(partitionId)
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
let hasPendingQueries = pendingCount > 0
|
|
1487
|
-
|
|
1488
|
-
if hasPendingQueries || isBehindTheHead {
|
|
1579
|
+
if (
|
|
1580
|
+
p.mutPendingQueries->Array.length > 0 || p.latestFetchedBlock.blockNumber < headBlockNumber
|
|
1581
|
+
) {
|
|
1489
1582
|
// Even if there are some partitions waiting for the new block
|
|
1490
1583
|
// We still want to wait for all partitions reaching the head
|
|
1491
1584
|
// because they might update knownHeight in their response
|
|
@@ -1493,51 +1586,86 @@ let getNextQuery = (
|
|
|
1493
1586
|
// and we don't want to poll the head for a few small partitions
|
|
1494
1587
|
shouldWaitForNewBlock := false
|
|
1495
1588
|
}
|
|
1589
|
+
}
|
|
1496
1590
|
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
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 {
|
|
1502
1602
|
| 0 => queryEndBlock
|
|
1503
1603
|
| _ =>
|
|
1504
1604
|
// Force head block as an endBlock when blockLag is set
|
|
1505
1605
|
// because otherwise HyperSync might return bigger range
|
|
1506
1606
|
Utils.Math.minOptInt(Some(headBlockNumber), queryEndBlock)
|
|
1507
1607
|
}
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
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
|
|
1516
1623
|
}
|
|
1624
|
+
existingReservedByPartition->Dict.set(partitionId, cost.contents)
|
|
1625
|
+
chainReserved := chainReserved.contents +. cost.contents
|
|
1626
|
+
}
|
|
1517
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)
|
|
1518
1641
|
let maybeChunkRange = getMinHistoryRange(p)
|
|
1519
1642
|
|
|
1520
|
-
// Walk pending queries to find open ranges and create queries for each
|
|
1521
1643
|
let cursor = ref(p.latestFetchedBlock.blockNumber + 1)
|
|
1522
1644
|
let canContinue = ref(true)
|
|
1645
|
+
let chunksUsedThisCall = ref(0)
|
|
1523
1646
|
let pqIdx = ref(0)
|
|
1524
|
-
while pqIdx.contents <
|
|
1647
|
+
while pqIdx.contents < pendingCount && canContinue.contents {
|
|
1525
1648
|
let pq = p.mutPendingQueries->Array.getUnsafe(pqIdx.contents)
|
|
1526
1649
|
|
|
1527
|
-
// Gap before this pending query → create queries for the gap range
|
|
1528
1650
|
if pq.fromBlock > cursor.contents {
|
|
1529
|
-
|
|
1530
|
-
|
|
1651
|
+
let beforeLen = bucket->Array.length
|
|
1652
|
+
let cost = pushGapFillQueries(
|
|
1653
|
+
bucket,
|
|
1531
1654
|
~partitionId,
|
|
1532
1655
|
~rangeFromBlock=cursor.contents,
|
|
1533
1656
|
~rangeEndBlock=Utils.Math.minOptInt(Some(pq.fromBlock - 1), queryEndBlock),
|
|
1534
|
-
~
|
|
1657
|
+
~knownHeight,
|
|
1658
|
+
~chainTargetBlock,
|
|
1535
1659
|
~maybeChunkRange,
|
|
1660
|
+
~maxChunks=maxPendingChunksPerPartition - pendingCount - chunksUsedThisCall.contents,
|
|
1536
1661
|
~partition=p,
|
|
1662
|
+
~partitionBudget=chainTargetItems /. partitionsCount->Int.toFloat,
|
|
1663
|
+
~chunkItemsMultiplier,
|
|
1537
1664
|
~selection=p.selection,
|
|
1538
1665
|
~addressesByContractName=p.addressesByContractName,
|
|
1539
|
-
~indexingAddresses,
|
|
1540
1666
|
)
|
|
1667
|
+
chunksUsedThisCall := chunksUsedThisCall.contents + (bucket->Array.length - beforeLen)
|
|
1668
|
+
gapFillCost := gapFillCost.contents +. cost
|
|
1541
1669
|
}
|
|
1542
1670
|
switch pq {
|
|
1543
1671
|
| {isChunk: true, toBlock: Some(toBlock), fetchedBlock: Some({blockNumber})}
|
|
@@ -1549,40 +1677,180 @@ let getNextQuery = (
|
|
|
1549
1677
|
pqIdx := pqIdx.contents + 1
|
|
1550
1678
|
}
|
|
1551
1679
|
|
|
1552
|
-
// Tail range after all pending queries
|
|
1553
1680
|
if canContinue.contents {
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
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
|
|
1566
1693
|
}
|
|
1694
|
+
}
|
|
1567
1695
|
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
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
|
|
1581
1714
|
}
|
|
1715
|
+
reservedByPartition->Dict.set(fs.partitionId, cost.contents)
|
|
1716
|
+
})
|
|
1582
1717
|
|
|
1583
|
-
|
|
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
|
|
1584
1846
|
}
|
|
1585
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
|
+
|
|
1586
1854
|
if queries->Utils.Array.isEmpty {
|
|
1587
1855
|
if shouldWaitForNewBlock.contents {
|
|
1588
1856
|
WaitingForNewBlock
|
|
@@ -1631,14 +1899,15 @@ Instantiates a fetch state with partitions for initial addresses
|
|
|
1631
1899
|
let make = (
|
|
1632
1900
|
~startBlock,
|
|
1633
1901
|
~endBlock,
|
|
1634
|
-
~
|
|
1902
|
+
~onEventRegistrations: array<Internal.onEventRegistration>,
|
|
1903
|
+
~contractConfigs: dict<IndexingAddresses.contractConfig>,
|
|
1635
1904
|
~addresses: array<Internal.indexingAddress>,
|
|
1636
1905
|
~maxAddrInPartition,
|
|
1637
1906
|
~chainId,
|
|
1638
1907
|
~maxOnBlockBufferSize,
|
|
1639
1908
|
~knownHeight,
|
|
1640
1909
|
~progressBlockNumber=startBlock - 1,
|
|
1641
|
-
~
|
|
1910
|
+
~onBlockRegistrations=[],
|
|
1642
1911
|
~blockLag=0,
|
|
1643
1912
|
~firstEventBlock=None,
|
|
1644
1913
|
): t => {
|
|
@@ -1648,38 +1917,15 @@ let make = (
|
|
|
1648
1917
|
}
|
|
1649
1918
|
|
|
1650
1919
|
let notDependingOnAddresses = []
|
|
1651
|
-
let
|
|
1920
|
+
let normalRegistrations = []
|
|
1652
1921
|
let contractNamesWithNormalEvents = Utils.Set.make()
|
|
1653
|
-
let indexingAddresses = Dict.make()
|
|
1654
|
-
let contractConfigs: dict<contractConfig> = Dict.make()
|
|
1655
|
-
|
|
1656
|
-
eventConfigs->Array.forEach(ec => {
|
|
1657
|
-
switch contractConfigs->Utils.Dict.dangerouslyGetNonOption(ec.contractName) {
|
|
1658
|
-
| Some({startBlock}) =>
|
|
1659
|
-
contractConfigs->Dict.set(
|
|
1660
|
-
ec.contractName,
|
|
1661
|
-
{
|
|
1662
|
-
startBlock: switch (startBlock, ec.startBlock) {
|
|
1663
|
-
| (Some(a), Some(b)) => Some(Pervasives.min(a, b))
|
|
1664
|
-
| (Some(_) as s, None) | (None, Some(_) as s) => s
|
|
1665
|
-
| (None, None) => None
|
|
1666
|
-
},
|
|
1667
|
-
},
|
|
1668
|
-
)
|
|
1669
|
-
| None =>
|
|
1670
|
-
contractConfigs->Dict.set(
|
|
1671
|
-
ec.contractName,
|
|
1672
|
-
{
|
|
1673
|
-
startBlock: ec.startBlock,
|
|
1674
|
-
},
|
|
1675
|
-
)
|
|
1676
|
-
}
|
|
1677
1922
|
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1923
|
+
onEventRegistrations->Array.forEach(reg => {
|
|
1924
|
+
if reg.dependsOnAddresses {
|
|
1925
|
+
normalRegistrations->Array.push(reg)
|
|
1926
|
+
contractNamesWithNormalEvents->Utils.Set.add(reg.eventConfig.contractName)->ignore
|
|
1681
1927
|
} else {
|
|
1682
|
-
notDependingOnAddresses->Array.push(
|
|
1928
|
+
notDependingOnAddresses->Array.push(reg)
|
|
1683
1929
|
}
|
|
1684
1930
|
})
|
|
1685
1931
|
|
|
@@ -1691,7 +1937,7 @@ let make = (
|
|
|
1691
1937
|
latestFetchedBlock,
|
|
1692
1938
|
selection: {
|
|
1693
1939
|
dependsOnAddresses: false,
|
|
1694
|
-
|
|
1940
|
+
onEventRegistrations: notDependingOnAddresses,
|
|
1695
1941
|
},
|
|
1696
1942
|
addressesByContractName: Dict.make(),
|
|
1697
1943
|
mergeBlock: None,
|
|
@@ -1706,7 +1952,7 @@ let make = (
|
|
|
1706
1952
|
|
|
1707
1953
|
let normalSelection = {
|
|
1708
1954
|
dependsOnAddresses: true,
|
|
1709
|
-
|
|
1955
|
+
onEventRegistrations: normalRegistrations,
|
|
1710
1956
|
}
|
|
1711
1957
|
|
|
1712
1958
|
let registeringContractsByContract: dict<dict<indexingAddress>> = Dict.make()
|
|
@@ -1714,32 +1960,16 @@ let make = (
|
|
|
1714
1960
|
|
|
1715
1961
|
addresses->Array.forEach(contract => {
|
|
1716
1962
|
let contractName = contract.contractName
|
|
1717
|
-
let contractStartBlock = switch contractConfigs->Utils.Dict.dangerouslyGetNonOption(
|
|
1718
|
-
contractName,
|
|
1719
|
-
) {
|
|
1720
|
-
| Some({startBlock}) => startBlock
|
|
1721
|
-
| None => None
|
|
1722
|
-
}
|
|
1723
|
-
let ia: indexingAddress = {
|
|
1724
|
-
address: contract.address,
|
|
1725
|
-
contractName: contract.contractName,
|
|
1726
|
-
registrationBlock: contract.registrationBlock,
|
|
1727
|
-
effectiveStartBlock: deriveEffectiveStartBlock(
|
|
1728
|
-
~registrationBlock=contract.registrationBlock,
|
|
1729
|
-
~contractStartBlock,
|
|
1730
|
-
),
|
|
1731
|
-
}
|
|
1732
|
-
// Track the address on fetchState regardless of whether it currently has
|
|
1733
|
-
// matching events. This way, if the config is updated later to add events
|
|
1734
|
-
// for this contract, the address is already known.
|
|
1735
|
-
indexingAddresses->Dict.set(contract.address->Address.toString, ia)
|
|
1736
1963
|
|
|
1737
1964
|
// Only addresses whose contract has events that depend on addresses get
|
|
1738
1965
|
// registered for active fetching via partitions.
|
|
1739
1966
|
if contractNamesWithNormalEvents->Utils.Set.has(contractName) {
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1967
|
+
registeringContractsByContract
|
|
1968
|
+
->Utils.Dict.getOrInsertEmptyDict(contractName)
|
|
1969
|
+
->Dict.set(
|
|
1970
|
+
contract.address->Address.toString,
|
|
1971
|
+
IndexingAddresses.makeIndexingAddress(~contract, ~contractConfigs),
|
|
1972
|
+
)
|
|
1743
1973
|
|
|
1744
1974
|
// Detect dynamic contracts by registrationBlock
|
|
1745
1975
|
if contract.registrationBlock !== -1 {
|
|
@@ -1758,12 +1988,15 @@ let make = (
|
|
|
1758
1988
|
~progressBlockNumber,
|
|
1759
1989
|
)
|
|
1760
1990
|
|
|
1761
|
-
if
|
|
1991
|
+
if (
|
|
1992
|
+
optimizedPartitions->OptimizedPartitions.count === 0 &&
|
|
1993
|
+
onBlockRegistrations->Utils.Array.isEmpty
|
|
1994
|
+
) {
|
|
1762
1995
|
JsError.throwWithMessage(
|
|
1763
1996
|
`Invalid configuration: Nothing to fetch on chain ${chainId->Int.toString}. ` ++
|
|
1764
1997
|
`addresses=${addresses->Array.length->Int.toString}, ` ++
|
|
1765
|
-
`
|
|
1766
|
-
`
|
|
1998
|
+
`onEventRegistrations=${onEventRegistrations->Array.length->Int.toString}, ` ++
|
|
1999
|
+
`normalRegistrations=${normalRegistrations
|
|
1767
2000
|
->Array.length
|
|
1768
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.`,
|
|
1769
2002
|
)
|
|
@@ -1774,14 +2007,14 @@ let make = (
|
|
|
1774
2007
|
// fetching, so without seeding the buffer here getNextQuery would return
|
|
1775
2008
|
// NothingToQuery and the indexer would get stuck.
|
|
1776
2009
|
let buffer = []
|
|
1777
|
-
let latestOnBlockBlockNumber = if knownHeight > 0 &&
|
|
2010
|
+
let latestOnBlockBlockNumber = if knownHeight > 0 && onBlockRegistrations->Utils.Array.notEmpty {
|
|
1778
2011
|
let maxBlockNumber = switch optimizedPartitions->OptimizedPartitions.getLatestFullyFetchedBlock {
|
|
1779
2012
|
| None => knownHeight
|
|
1780
2013
|
| Some(latestFullyFetchedBlock) => latestFullyFetchedBlock.blockNumber
|
|
1781
2014
|
}
|
|
1782
2015
|
appendOnBlockItems(
|
|
1783
2016
|
~mutItems=buffer,
|
|
1784
|
-
~
|
|
2017
|
+
~onBlockRegistrations,
|
|
1785
2018
|
~indexerStartBlock=startBlock,
|
|
1786
2019
|
~fromBlock=progressBlockNumber,
|
|
1787
2020
|
~maxBlockNumber,
|
|
@@ -1799,17 +2032,14 @@ let make = (
|
|
|
1799
2032
|
endBlock,
|
|
1800
2033
|
latestOnBlockBlockNumber,
|
|
1801
2034
|
normalSelection,
|
|
1802
|
-
indexingAddresses,
|
|
1803
2035
|
blockLag,
|
|
1804
|
-
|
|
2036
|
+
onBlockRegistrations,
|
|
1805
2037
|
maxOnBlockBufferSize,
|
|
1806
2038
|
knownHeight,
|
|
1807
2039
|
buffer,
|
|
1808
2040
|
firstEventBlock,
|
|
1809
2041
|
}
|
|
1810
2042
|
|
|
1811
|
-
let numAddresses = indexingAddresses->Utils.Dict.size
|
|
1812
|
-
Prometheus.IndexingAddresses.set(~addressesCount=numAddresses, ~chainId)
|
|
1813
2043
|
Prometheus.IndexingPartitions.set(
|
|
1814
2044
|
~partitionsCount=optimizedPartitions->OptimizedPartitions.count,
|
|
1815
2045
|
~chainId,
|
|
@@ -1826,25 +2056,6 @@ let make = (
|
|
|
1826
2056
|
|
|
1827
2057
|
let bufferSize = ({buffer}: t) => buffer->Array.length
|
|
1828
2058
|
|
|
1829
|
-
// Number of buffered items at or below the ready frontier (processable now,
|
|
1830
|
-
// i.e. not stuck behind a gap from a lagging partition or out-of-order chunk).
|
|
1831
|
-
// The buffer is kept sorted, so binary-search the frontier in O(log n).
|
|
1832
|
-
let bufferReadyCount = (fetchState: t) => {
|
|
1833
|
-
let frontier = fetchState->bufferBlockNumber
|
|
1834
|
-
let buffer = fetchState.buffer
|
|
1835
|
-
let lo = ref(0)
|
|
1836
|
-
let hi = ref(buffer->Array.length)
|
|
1837
|
-
while lo.contents < hi.contents {
|
|
1838
|
-
let mid = (lo.contents + hi.contents) / 2
|
|
1839
|
-
if buffer->Array.getUnsafe(mid)->Internal.getItemBlockNumber <= frontier {
|
|
1840
|
-
lo := mid + 1
|
|
1841
|
-
} else {
|
|
1842
|
-
hi := mid
|
|
1843
|
-
}
|
|
1844
|
-
}
|
|
1845
|
-
lo.contents
|
|
1846
|
-
}
|
|
1847
|
-
|
|
1848
2059
|
let rollbackPendingQueries = (mutPendingQueries: array<pendingQuery>, ~targetBlockNumber) => {
|
|
1849
2060
|
// - Remove queries where fromBlock > target
|
|
1850
2061
|
// - Cap fetchedBlock at target where fetchedBlock > target
|
|
@@ -1876,18 +2087,11 @@ Always recreates optimized partitions to avoid duplicate addresses:
|
|
|
1876
2087
|
- Non-wildcard with lfb <= target: keep, adjust pending queries and mergeBlock
|
|
1877
2088
|
- Non-wildcard with lfb > target: delete, track addresses for recreation
|
|
1878
2089
|
*/
|
|
1879
|
-
let rollback = (fetchState: t, ~targetBlockNumber) => {
|
|
1880
|
-
// Step 1:
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
fetchState.indexingAddresses->Utils.Dict.forEachWithKey((indexingContract, address) => {
|
|
1885
|
-
if indexingContract.registrationBlock > targetBlockNumber {
|
|
1886
|
-
let _ = addressesToRemove->Utils.Set.add(address->Address.unsafeFromString)
|
|
1887
|
-
} else {
|
|
1888
|
-
indexingAddresses->Dict.set(address, indexingContract)
|
|
1889
|
-
}
|
|
1890
|
-
})
|
|
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)
|
|
1891
2095
|
|
|
1892
2096
|
// Step 2: Categorize partitions
|
|
1893
2097
|
let keptPartitions = []
|
|
@@ -1917,18 +2121,12 @@ let rollback = (fetchState: t, ~targetBlockNumber) => {
|
|
|
1917
2121
|
| _ if p.latestFetchedBlock.blockNumber > targetBlockNumber =>
|
|
1918
2122
|
p.addressesByContractName->Utils.Dict.forEachWithKey((addresses, contractName) => {
|
|
1919
2123
|
addresses->Array.forEach(address => {
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
indexingAddresses
|
|
1923
|
-
->Utils.Dict.dangerouslyGetNonOption(address->Address.toString)
|
|
1924
|
-
->Option.isSome
|
|
1925
|
-
) {
|
|
2124
|
+
switch indexingAddresses->IndexingAddresses.get(address->Address.toString) {
|
|
2125
|
+
| Some(indexingContract) =>
|
|
1926
2126
|
let registeringContracts =
|
|
1927
2127
|
registeringContractsByContract->Utils.Dict.getOrInsertEmptyDict(contractName)
|
|
1928
|
-
registeringContracts->Dict.set(
|
|
1929
|
-
|
|
1930
|
-
indexingAddresses->Dict.getUnsafe(address->Address.toString),
|
|
1931
|
-
)
|
|
2128
|
+
registeringContracts->Dict.set(address->Address.toString, indexingContract)
|
|
2129
|
+
| None => ()
|
|
1932
2130
|
}
|
|
1933
2131
|
})
|
|
1934
2132
|
})
|
|
@@ -1941,11 +2139,13 @@ let rollback = (fetchState: t, ~targetBlockNumber) => {
|
|
|
1941
2139
|
| other => other
|
|
1942
2140
|
}
|
|
1943
2141
|
|
|
1944
|
-
//
|
|
2142
|
+
// Drop addresses pruned from the index
|
|
1945
2143
|
let rollbackedAddressesByContractName = Dict.make()
|
|
1946
2144
|
addressesByContractName->Utils.Dict.forEachWithKey((addresses, contractName) => {
|
|
1947
2145
|
let keptAddresses =
|
|
1948
|
-
addresses->Array.filter(address =>
|
|
2146
|
+
addresses->Array.filter(address =>
|
|
2147
|
+
indexingAddresses->IndexingAddresses.get(address->Address.toString)->Option.isSome
|
|
2148
|
+
)
|
|
1949
2149
|
if keptAddresses->Array.length > 0 {
|
|
1950
2150
|
rollbackedAddressesByContractName->Dict.set(contractName, keptAddresses)
|
|
1951
2151
|
}
|
|
@@ -1989,7 +2189,6 @@ let rollback = (fetchState: t, ~targetBlockNumber) => {
|
|
|
1989
2189
|
),
|
|
1990
2190
|
}->updateInternal(
|
|
1991
2191
|
~optimizedPartitions,
|
|
1992
|
-
~indexingAddresses,
|
|
1993
2192
|
~mutItems=fetchState.buffer->Array.filter(item =>
|
|
1994
2193
|
switch item {
|
|
1995
2194
|
| Event({blockNumber})
|
|
@@ -2084,17 +2283,6 @@ let getProgressPercentage = (fetchState: t) => {
|
|
|
2084
2283
|
}
|
|
2085
2284
|
}
|
|
2086
2285
|
|
|
2087
|
-
// Progress a specific block sits at along the chain, used to order queries from
|
|
2088
|
-
// different chains: a query starting further back (lower %) is fetched first.
|
|
2089
|
-
let getProgressPercentageAt = (fetchState: t, ~blockNumber) => {
|
|
2090
|
-
switch fetchState.firstEventBlock {
|
|
2091
|
-
| None => 0.
|
|
2092
|
-
| Some(firstEventBlock) =>
|
|
2093
|
-
let totalRange = fetchState.knownHeight - firstEventBlock
|
|
2094
|
-
totalRange <= 0 ? 0. : (blockNumber - firstEventBlock)->Int.toFloat /. totalRange->Int.toFloat
|
|
2095
|
-
}
|
|
2096
|
-
}
|
|
2097
|
-
|
|
2098
2286
|
let sortForBatch = {
|
|
2099
2287
|
let hasFullBatch = ({buffer} as fetchState: t, ~batchSizeTarget) => {
|
|
2100
2288
|
switch buffer->Array.get(batchSizeTarget - 1) {
|