envio 3.3.0-alpha.1 → 3.3.0-alpha.11
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/licenses/CLA.md +35 -0
- package/licenses/EULA.md +67 -0
- package/licenses/LICENSE.md +45 -0
- package/licenses/README.md +35 -0
- package/package.json +10 -9
- 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 +53 -16
- package/src/ChainFetching.res.mjs +37 -19
- package/src/ChainMetadata.res +1 -11
- package/src/ChainMetadata.res.mjs +1 -10
- package/src/ChainState.res +630 -161
- package/src/ChainState.res.mjs +438 -101
- package/src/ChainState.resi +77 -4
- package/src/Config.res +70 -38
- package/src/Config.res.mjs +52 -12
- package/src/ContractRegisterContext.res +2 -12
- package/src/ContractRegisterContext.res.mjs +3 -5
- package/src/Core.res +34 -3
- package/src/Core.res.mjs +12 -0
- package/src/CrossChainState.res +130 -89
- package/src/CrossChainState.res.mjs +66 -61
- 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 +191 -127
- package/src/EventConfigBuilder.res.mjs +116 -66
- package/src/EventProcessing.res +77 -28
- package/src/EventProcessing.res.mjs +56 -29
- package/src/ExitOnCaughtUp.res +10 -2
- package/src/ExitOnCaughtUp.res.mjs +10 -1
- package/src/FetchState.res +684 -415
- package/src/FetchState.res.mjs +505 -486
- package/src/HandlerLoader.res +8 -104
- package/src/HandlerLoader.res.mjs +2 -81
- package/src/HandlerRegister.res +520 -144
- package/src/HandlerRegister.res.mjs +286 -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 +166 -48
- package/src/Internal.res.mjs +30 -2
- package/src/LoadLayer.res +5 -5
- package/src/LoadLayer.res.mjs +6 -6
- package/src/LogSelection.res +92 -217
- package/src/LogSelection.res.mjs +95 -184
- 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 +128 -28
- package/src/SimulateItems.res.mjs +82 -40
- package/src/TestIndexer.res +55 -41
- package/src/TestIndexer.res.mjs +39 -32
- package/src/TopicFilter.res +1 -25
- package/src/TopicFilter.res.mjs +0 -74
- 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 -46
- 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 +31 -49
- package/src/sources/EventRouter.res.mjs +12 -25
- package/src/sources/Evm.res +83 -72
- package/src/sources/Evm.res.mjs +53 -69
- package/src/sources/EvmChain.res +7 -34
- package/src/sources/EvmChain.res.mjs +6 -27
- package/src/sources/EvmRpcClient.res +71 -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 +45 -42
- package/src/sources/HyperSync.res.mjs +68 -60
- package/src/sources/HyperSync.resi +14 -4
- package/src/sources/HyperSyncClient.res +107 -119
- package/src/sources/HyperSyncClient.res.mjs +36 -31
- package/src/sources/HyperSyncHeightStream.res +1 -8
- package/src/sources/HyperSyncHeightStream.res.mjs +0 -2
- package/src/sources/HyperSyncSource.res +60 -254
- package/src/sources/HyperSyncSource.res.mjs +57 -180
- package/src/sources/Rpc.res +15 -79
- package/src/sources/Rpc.res.mjs +25 -101
- package/src/sources/RpcSource.res +221 -514
- package/src/sources/RpcSource.res.mjs +216 -411
- package/src/sources/RpcWebSocketHeightStream.res +0 -5
- package/src/sources/RpcWebSocketHeightStream.res.mjs +0 -2
- package/src/sources/SimulateSource.res +44 -20
- package/src/sources/SimulateSource.res.mjs +41 -14
- 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,30 +607,124 @@ 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
|
*/
|
|
581
|
-
let
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
632
|
+
let getRegistrationIndex = (item: Internal.item): int =>
|
|
633
|
+
switch item {
|
|
634
|
+
| Event({onEventRegistration}) => onEventRegistration.index
|
|
635
|
+
| Block({onBlockRegistration}) => onBlockRegistration.index
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Total order on buffer items: block, then logIndex, then registration index.
|
|
639
|
+
// Returns a plain int (-1/0/1) with explicit field comparisons so it can be
|
|
640
|
+
// called directly from the merge/insertion loops below — no Array.sort callback,
|
|
641
|
+
// no allocated key. `0` means a true duplicate: same log routed to the same
|
|
642
|
+
// registration (two registrations for one log differ by index and are kept).
|
|
643
|
+
let compareBufferItem = (a: Internal.item, b: Internal.item): int => {
|
|
644
|
+
let ba = a->Internal.getItemBlockNumber
|
|
645
|
+
let bb = b->Internal.getItemBlockNumber
|
|
646
|
+
if ba != bb {
|
|
647
|
+
ba < bb ? -1 : 1
|
|
585
648
|
} else {
|
|
586
|
-
|
|
649
|
+
let la = a->Internal.getItemLogIndex
|
|
650
|
+
let lb = b->Internal.getItemLogIndex
|
|
651
|
+
if la != lb {
|
|
652
|
+
la < lb ? -1 : 1
|
|
653
|
+
} else {
|
|
654
|
+
let ia = a->getRegistrationIndex
|
|
655
|
+
let ib = b->getRegistrationIndex
|
|
656
|
+
ia < ib ? -1 : ia > ib ? 1 : 0
|
|
657
|
+
}
|
|
587
658
|
}
|
|
588
659
|
}
|
|
589
660
|
|
|
661
|
+
// Merge a maybe-unsorted `newItems` run into the already-sorted, already-deduped
|
|
662
|
+
// `buffer`, dropping items equal on (blockNumber, logIndex, registration index).
|
|
663
|
+
// Single linear pass over both runs after ordering `newItems` in place; every
|
|
664
|
+
// comparison is a direct `compareBufferItem` call (V8 inlines it) rather than a
|
|
665
|
+
// callback through `Array.sort`.
|
|
666
|
+
let mergeIntoBuffer = (buffer: array<Internal.item>, newItems: array<Internal.item>): array<
|
|
667
|
+
Internal.item,
|
|
668
|
+
> => {
|
|
669
|
+
let n = newItems->Array.length
|
|
670
|
+
// Insertion sort: a source response is small and usually already ascending,
|
|
671
|
+
// so this is ~O(n) here.
|
|
672
|
+
for i in 1 to n - 1 {
|
|
673
|
+
let x = newItems->Array.getUnsafe(i)
|
|
674
|
+
let j = ref(i - 1)
|
|
675
|
+
while j.contents >= 0 && compareBufferItem(newItems->Array.getUnsafe(j.contents), x) > 0 {
|
|
676
|
+
newItems->Array.setUnsafe(j.contents + 1, newItems->Array.getUnsafe(j.contents))
|
|
677
|
+
j := j.contents - 1
|
|
678
|
+
}
|
|
679
|
+
newItems->Array.setUnsafe(j.contents + 1, x)
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
let m = buffer->Array.length
|
|
683
|
+
let merged = []
|
|
684
|
+
let last = ref(None)
|
|
685
|
+
let push = item =>
|
|
686
|
+
switch last.contents {
|
|
687
|
+
| Some(l) if compareBufferItem(l, item) === 0 => ()
|
|
688
|
+
| _ => {
|
|
689
|
+
merged->Array.push(item)
|
|
690
|
+
last := Some(item)
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
let i = ref(0)
|
|
695
|
+
let j = ref(0)
|
|
696
|
+
while i.contents < m && j.contents < n {
|
|
697
|
+
let a = buffer->Array.getUnsafe(i.contents)
|
|
698
|
+
let b = newItems->Array.getUnsafe(j.contents)
|
|
699
|
+
if compareBufferItem(a, b) <= 0 {
|
|
700
|
+
push(a)
|
|
701
|
+
i := i.contents + 1
|
|
702
|
+
} else {
|
|
703
|
+
push(b)
|
|
704
|
+
j := j.contents + 1
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
while i.contents < m {
|
|
708
|
+
push(buffer->Array.getUnsafe(i.contents))
|
|
709
|
+
i := i.contents + 1
|
|
710
|
+
}
|
|
711
|
+
while j.contents < n {
|
|
712
|
+
push(newItems->Array.getUnsafe(j.contents))
|
|
713
|
+
j := j.contents + 1
|
|
714
|
+
}
|
|
715
|
+
merged
|
|
716
|
+
}
|
|
717
|
+
|
|
590
718
|
// Some big number which should be bigger than any log index
|
|
591
719
|
let blockItemLogIndex = 16777216
|
|
592
720
|
|
|
593
|
-
let numAddresses = fetchState => fetchState.indexingAddresses->Utils.Dict.size
|
|
594
|
-
|
|
595
721
|
// Appends Block items produced by the onBlock handlers for every block in
|
|
596
722
|
// (fromBlock, maxBlockNumber] into mutItems and returns the new
|
|
597
723
|
// latestOnBlockBlockNumber pointer. maxOnBlockBufferSize bounds how many items
|
|
598
724
|
// are generated at once to prevent OOM.
|
|
599
725
|
let appendOnBlockItems = (
|
|
600
726
|
~mutItems: array<Internal.item>,
|
|
601
|
-
~
|
|
727
|
+
~onBlockRegistrations: array<Internal.onBlockRegistration>,
|
|
602
728
|
~indexerStartBlock,
|
|
603
729
|
~fromBlock,
|
|
604
730
|
~maxBlockNumber,
|
|
@@ -618,27 +744,27 @@ let appendOnBlockItems = (
|
|
|
618
744
|
let blockNumber = latestOnBlockBlockNumber.contents + 1
|
|
619
745
|
latestOnBlockBlockNumber := blockNumber
|
|
620
746
|
|
|
621
|
-
for configIdx in 0 to
|
|
622
|
-
let
|
|
747
|
+
for configIdx in 0 to onBlockRegistrations->Array.length - 1 {
|
|
748
|
+
let onBlockRegistration = onBlockRegistrations->Array.getUnsafe(configIdx)
|
|
623
749
|
|
|
624
|
-
let handlerStartBlock = switch
|
|
750
|
+
let handlerStartBlock = switch onBlockRegistration.startBlock {
|
|
625
751
|
| Some(startBlock) => startBlock
|
|
626
752
|
| None => indexerStartBlock
|
|
627
753
|
}
|
|
628
754
|
|
|
629
755
|
if (
|
|
630
756
|
blockNumber >= handlerStartBlock &&
|
|
631
|
-
switch
|
|
757
|
+
switch onBlockRegistration.endBlock {
|
|
632
758
|
| Some(endBlock) => blockNumber <= endBlock
|
|
633
759
|
| None => true
|
|
634
760
|
} &&
|
|
635
|
-
(blockNumber - handlerStartBlock)->Pervasives.mod(
|
|
761
|
+
(blockNumber - handlerStartBlock)->Pervasives.mod(onBlockRegistration.interval) === 0
|
|
636
762
|
) {
|
|
637
763
|
mutItems->Array.push(
|
|
638
764
|
Block({
|
|
639
|
-
|
|
765
|
+
onBlockRegistration,
|
|
640
766
|
blockNumber,
|
|
641
|
-
logIndex: blockItemLogIndex +
|
|
767
|
+
logIndex: blockItemLogIndex + onBlockRegistration.index,
|
|
642
768
|
}),
|
|
643
769
|
)
|
|
644
770
|
newItemsCounter := newItemsCounter.contents + 1
|
|
@@ -656,50 +782,47 @@ Runs partition optimization when partitions change.
|
|
|
656
782
|
let updateInternal = (
|
|
657
783
|
fetchState: t,
|
|
658
784
|
~optimizedPartitions=fetchState.optimizedPartitions,
|
|
659
|
-
~indexingAddresses=fetchState.indexingAddresses,
|
|
660
785
|
~mutItems=?,
|
|
786
|
+
// Set when the caller already passes a sorted, deduped buffer (hot paths merge
|
|
787
|
+
// via mergeIntoBuffer or filter the sorted buffer). Otherwise mutItems is
|
|
788
|
+
// normalized here, so callers can hand over items in any order.
|
|
789
|
+
~mutItemsSorted=false,
|
|
661
790
|
~blockLag=fetchState.blockLag,
|
|
662
791
|
~knownHeight=fetchState.knownHeight,
|
|
663
792
|
): t => {
|
|
664
|
-
|
|
793
|
+
// The buffer to build on: the caller's items (normalized to sorted if needed),
|
|
794
|
+
// or the current buffer when only onBlock items change.
|
|
795
|
+
let base = switch mutItems {
|
|
796
|
+
| Some(items) => mutItemsSorted ? items : []->mergeIntoBuffer(items)
|
|
797
|
+
| None => fetchState.buffer
|
|
798
|
+
}
|
|
665
799
|
|
|
666
|
-
|
|
800
|
+
// onBlock items are generated as their own ascending (block, logIndex) run and
|
|
801
|
+
// folded into `base` by the single merge below.
|
|
802
|
+
let blockItems = []
|
|
803
|
+
let latestOnBlockBlockNumber = switch fetchState.onBlockRegistrations {
|
|
667
804
|
| [] => knownHeight
|
|
668
|
-
|
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
|
678
|
-
|
|
|
679
|
-
}->Array.get(fetchState.maxOnBlockBufferSize - 1) {
|
|
680
|
-
| Some(item) => item->Internal.getItemBlockNumber
|
|
681
|
-
| None =>
|
|
682
|
-
switch optimizedPartitions->OptimizedPartitions.getLatestFullyFetchedBlock {
|
|
683
|
-
| None => knownHeight
|
|
684
|
-
| Some(latestFullyFetchedBlock) => latestFullyFetchedBlock.blockNumber
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
let mutItems = switch mutItemsRef.contents {
|
|
689
|
-
| Some(mutItems) => mutItems
|
|
690
|
-
| None => fetchState.buffer->Array.copy
|
|
805
|
+
| onBlockRegistrations =>
|
|
806
|
+
// Calculate the max block number we are going to create items for
|
|
807
|
+
// Use maxOnBlockBufferSize to get the last target item in the buffer
|
|
808
|
+
// (sorted, so this is the highest-block item within the buffer cap).
|
|
809
|
+
// All this needed to prevent OOM when adding too many block items to the queue
|
|
810
|
+
let maxBlockNumber = switch base->Array.get(fetchState.maxOnBlockBufferSize - 1) {
|
|
811
|
+
| Some(item) => item->Internal.getItemBlockNumber
|
|
812
|
+
| None =>
|
|
813
|
+
switch optimizedPartitions->OptimizedPartitions.getLatestFullyFetchedBlock {
|
|
814
|
+
| None => knownHeight
|
|
815
|
+
| Some(latestFullyFetchedBlock) => latestFullyFetchedBlock.blockNumber
|
|
691
816
|
}
|
|
692
|
-
mutItemsRef := Some(mutItems)
|
|
693
|
-
|
|
694
|
-
appendOnBlockItems(
|
|
695
|
-
~mutItems,
|
|
696
|
-
~onBlockConfigs,
|
|
697
|
-
~indexerStartBlock=fetchState.startBlock,
|
|
698
|
-
~fromBlock=fetchState.latestOnBlockBlockNumber,
|
|
699
|
-
~maxBlockNumber,
|
|
700
|
-
~maxOnBlockBufferSize=fetchState.maxOnBlockBufferSize,
|
|
701
|
-
)
|
|
702
817
|
}
|
|
818
|
+
appendOnBlockItems(
|
|
819
|
+
~mutItems=blockItems,
|
|
820
|
+
~onBlockRegistrations,
|
|
821
|
+
~indexerStartBlock=fetchState.startBlock,
|
|
822
|
+
~fromBlock=fetchState.latestOnBlockBlockNumber,
|
|
823
|
+
~maxBlockNumber,
|
|
824
|
+
~maxOnBlockBufferSize=fetchState.maxOnBlockBufferSize,
|
|
825
|
+
)
|
|
703
826
|
}
|
|
704
827
|
|
|
705
828
|
let updatedFetchState = {
|
|
@@ -708,22 +831,16 @@ let updateInternal = (
|
|
|
708
831
|
contractConfigs: fetchState.contractConfigs,
|
|
709
832
|
normalSelection: fetchState.normalSelection,
|
|
710
833
|
chainId: fetchState.chainId,
|
|
711
|
-
|
|
834
|
+
onBlockRegistrations: fetchState.onBlockRegistrations,
|
|
712
835
|
maxOnBlockBufferSize: fetchState.maxOnBlockBufferSize,
|
|
713
836
|
optimizedPartitions,
|
|
714
837
|
latestOnBlockBlockNumber,
|
|
715
|
-
indexingAddresses,
|
|
716
838
|
blockLag,
|
|
717
839
|
knownHeight,
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
| Some(mutItems) => {
|
|
723
|
-
mutItems->Array.sort(compareBufferItem)
|
|
724
|
-
mutItems
|
|
725
|
-
}
|
|
726
|
-
| None => fetchState.buffer
|
|
840
|
+
// Single merge point: fold any onBlock items into the sorted base buffer.
|
|
841
|
+
buffer: switch blockItems {
|
|
842
|
+
| [] => base
|
|
843
|
+
| blockItems => base->mergeIntoBuffer(blockItems)
|
|
727
844
|
},
|
|
728
845
|
firstEventBlock: fetchState.firstEventBlock,
|
|
729
846
|
}
|
|
@@ -740,12 +857,6 @@ let updateInternal = (
|
|
|
740
857
|
~blockNumber=updatedFetchState->bufferBlockNumber,
|
|
741
858
|
~chainId=fetchState.chainId,
|
|
742
859
|
)
|
|
743
|
-
if indexingAddresses !== fetchState.indexingAddresses {
|
|
744
|
-
Prometheus.IndexingAddresses.set(
|
|
745
|
-
~addressesCount=updatedFetchState->numAddresses,
|
|
746
|
-
~chainId=fetchState.chainId,
|
|
747
|
-
)
|
|
748
|
-
}
|
|
749
860
|
|
|
750
861
|
updatedFetchState
|
|
751
862
|
}
|
|
@@ -976,22 +1087,22 @@ OptimizedPartitions.t => {
|
|
|
976
1087
|
|
|
977
1088
|
let registerDynamicContracts = (
|
|
978
1089
|
fetchState: t,
|
|
1090
|
+
~indexingAddresses: IndexingAddresses.t,
|
|
979
1091
|
// These are raw items which might have dynamic contracts received from contractRegister call.
|
|
980
1092
|
// Might contain duplicates which we should filter out
|
|
981
1093
|
items: array<Internal.item>,
|
|
982
1094
|
) => {
|
|
983
|
-
if fetchState.normalSelection.
|
|
1095
|
+
if fetchState.normalSelection.onEventRegistrations->Utils.Array.isEmpty {
|
|
984
1096
|
// Can the normalSelection be empty?
|
|
985
1097
|
JsError.throwWithMessage(
|
|
986
1098
|
"Invalid configuration. No events to fetch for the dynamic contract registration.",
|
|
987
1099
|
)
|
|
988
1100
|
}
|
|
989
1101
|
|
|
990
|
-
let indexingAddresses = fetchState.indexingAddresses
|
|
991
1102
|
let registeringContractsByContract: dict<dict<indexingAddress>> = Dict.make()
|
|
992
1103
|
let earliestRegisteringEventBlockNumber = ref(%raw(`Infinity`))
|
|
993
1104
|
// Addresses registered for contracts without matching events. These are not
|
|
994
|
-
// added to partitions, but they are tracked on
|
|
1105
|
+
// added to partitions, but they are tracked on indexingAddresses
|
|
995
1106
|
// so that later conflicting registrations are detected, and are persisted
|
|
996
1107
|
// to envio_addresses so they can be picked up on restart with updated config.
|
|
997
1108
|
let noEventsAddresses: dict<indexingAddress> = Dict.make()
|
|
@@ -1017,15 +1128,13 @@ let registerDynamicContracts = (
|
|
|
1017
1128
|
address: dc.address,
|
|
1018
1129
|
contractName: dc.contractName,
|
|
1019
1130
|
registrationBlock: dc.registrationBlock,
|
|
1020
|
-
effectiveStartBlock: deriveEffectiveStartBlock(
|
|
1131
|
+
effectiveStartBlock: IndexingAddresses.deriveEffectiveStartBlock(
|
|
1021
1132
|
~registrationBlock=dc.registrationBlock,
|
|
1022
1133
|
~contractStartBlock,
|
|
1023
1134
|
),
|
|
1024
1135
|
}
|
|
1025
1136
|
// Prevent registering already indexing contracts
|
|
1026
|
-
switch indexingAddresses->
|
|
1027
|
-
dc.address->Address.toString,
|
|
1028
|
-
) {
|
|
1137
|
+
switch indexingAddresses->IndexingAddresses.get(dc.address->Address.toString) {
|
|
1029
1138
|
| Some(existingContract) =>
|
|
1030
1139
|
// FIXME: Instead of filtering out duplicates,
|
|
1031
1140
|
// we should check the block number first.
|
|
@@ -1080,7 +1189,7 @@ let registerDynamicContracts = (
|
|
|
1080
1189
|
address: dc.address,
|
|
1081
1190
|
contractName: dc.contractName,
|
|
1082
1191
|
registrationBlock: dc.registrationBlock,
|
|
1083
|
-
effectiveStartBlock: deriveEffectiveStartBlock(
|
|
1192
|
+
effectiveStartBlock: IndexingAddresses.deriveEffectiveStartBlock(
|
|
1084
1193
|
~registrationBlock=dc.registrationBlock,
|
|
1085
1194
|
~contractStartBlock=None,
|
|
1086
1195
|
),
|
|
@@ -1088,9 +1197,7 @@ let registerDynamicContracts = (
|
|
|
1088
1197
|
// Prevent duplicate logging/persistence when the same address is
|
|
1089
1198
|
// already tracked on fetchState, either from the db on startup or
|
|
1090
1199
|
// from an earlier registration in this batch.
|
|
1091
|
-
switch indexingAddresses->
|
|
1092
|
-
dc.address->Address.toString,
|
|
1093
|
-
) {
|
|
1200
|
+
switch indexingAddresses->IndexingAddresses.get(dc.address->Address.toString) {
|
|
1094
1201
|
| Some(existingContract) =>
|
|
1095
1202
|
if existingContract.contractName != dc.contractName {
|
|
1096
1203
|
fetchState->warnDifferentContractType(~existingContract, ~dc=dcAsIndexingAddress)
|
|
@@ -1144,12 +1251,10 @@ let registerDynamicContracts = (
|
|
|
1144
1251
|
// Only dcs for contracts without events. Track them on
|
|
1145
1252
|
// indexingAddresses so subsequent registrations see them, but don't touch
|
|
1146
1253
|
// partitions since there's nothing to fetch for them.
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
fetchState->updateInternal(~indexingAddresses=newIndexingContracts)
|
|
1254
|
+
indexingAddresses->IndexingAddresses.register(noEventsAddresses)
|
|
1255
|
+
fetchState
|
|
1150
1256
|
| (_, _) => {
|
|
1151
1257
|
let newPartitions = []
|
|
1152
|
-
let newIndexingAddresses = indexingAddresses->Utils.Dict.shallowCopy
|
|
1153
1258
|
let dynamicContractsRef = ref(fetchState.optimizedPartitions.dynamicContracts)
|
|
1154
1259
|
let mutExistingPartitions = fetchState.optimizedPartitions.entities->Dict.valuesToArray
|
|
1155
1260
|
|
|
@@ -1231,10 +1336,10 @@ let registerDynamicContracts = (
|
|
|
1231
1336
|
}
|
|
1232
1337
|
|
|
1233
1338
|
let registeringContracts = registeringContractsByContract->Dict.getUnsafe(contractName)
|
|
1234
|
-
|
|
1339
|
+
indexingAddresses->IndexingAddresses.register(registeringContracts)
|
|
1235
1340
|
}
|
|
1236
1341
|
// Include no-events dcs so later batches detect conflicts against them.
|
|
1237
|
-
|
|
1342
|
+
indexingAddresses->IndexingAddresses.register(noEventsAddresses)
|
|
1238
1343
|
|
|
1239
1344
|
let optimizedPartitions = createPartitionsFromIndexingAddresses(
|
|
1240
1345
|
~registeringContractsByContract,
|
|
@@ -1247,11 +1352,31 @@ let registerDynamicContracts = (
|
|
|
1247
1352
|
~progressBlockNumber=0,
|
|
1248
1353
|
)
|
|
1249
1354
|
|
|
1250
|
-
fetchState->updateInternal(~optimizedPartitions
|
|
1355
|
+
fetchState->updateInternal(~optimizedPartitions)
|
|
1251
1356
|
}
|
|
1252
1357
|
}
|
|
1253
1358
|
}
|
|
1254
1359
|
|
|
1360
|
+
// Drop events an address-param filter rejects. A merged partition may
|
|
1361
|
+
// over-fetch a wildcard event whose indexed address param references an
|
|
1362
|
+
// address registered after the log's block; `clientAddressFilter` is the
|
|
1363
|
+
// param-level analogue of EventRouter's srcAddress effectiveStartBlock check.
|
|
1364
|
+
let filterByClientAddress = (
|
|
1365
|
+
items: array<Internal.item>,
|
|
1366
|
+
~indexingAddresses: IndexingAddresses.t,
|
|
1367
|
+
): array<Internal.item> =>
|
|
1368
|
+
items->Array.filter(item =>
|
|
1369
|
+
switch item {
|
|
1370
|
+
| Internal.Event({payload, blockNumber}) as item =>
|
|
1371
|
+
switch (item->Internal.castUnsafeEventItem).onEventRegistration.clientAddressFilter {
|
|
1372
|
+
| Some(filter) =>
|
|
1373
|
+
filter(payload, blockNumber, indexingAddresses->IndexingAddresses.rawForFilter)
|
|
1374
|
+
| None => true
|
|
1375
|
+
}
|
|
1376
|
+
| _ => true
|
|
1377
|
+
}
|
|
1378
|
+
)
|
|
1379
|
+
|
|
1255
1380
|
/*
|
|
1256
1381
|
Updates fetchState with a response for a given query.
|
|
1257
1382
|
Returns Error if the partition with given query cannot be found (unexpected)
|
|
@@ -1264,20 +1389,6 @@ let handleQueryResult = (
|
|
|
1264
1389
|
~latestFetchedBlock: blockNumberAndTimestamp,
|
|
1265
1390
|
~newItems,
|
|
1266
1391
|
): t => {
|
|
1267
|
-
// Drop events an address-param filter rejects. A merged partition may
|
|
1268
|
-
// over-fetch a wildcard event whose indexed address param references an
|
|
1269
|
-
// address registered after the log's block; `clientAddressFilter` is the
|
|
1270
|
-
// param-level analogue of EventRouter's srcAddress effectiveStartBlock check.
|
|
1271
|
-
let newItems = newItems->Array.filter(item =>
|
|
1272
|
-
switch item {
|
|
1273
|
-
| Internal.Event({eventConfig, payload, blockNumber}) =>
|
|
1274
|
-
switch eventConfig.clientAddressFilter {
|
|
1275
|
-
| Some(filter) => filter(payload, blockNumber, fetchState.indexingAddresses)
|
|
1276
|
-
| None => true
|
|
1277
|
-
}
|
|
1278
|
-
| _ => true
|
|
1279
|
-
}
|
|
1280
|
-
)
|
|
1281
1392
|
fetchState->updateInternal(
|
|
1282
1393
|
~optimizedPartitions=fetchState.optimizedPartitions->OptimizedPartitions.handleQueryResponse(
|
|
1283
1394
|
~query,
|
|
@@ -1285,10 +1396,14 @@ let handleQueryResult = (
|
|
|
1285
1396
|
~itemsCount=newItems->Array.length,
|
|
1286
1397
|
~latestFetchedBlock,
|
|
1287
1398
|
),
|
|
1399
|
+
// Merge the response into the sorted buffer, dropping duplicates an
|
|
1400
|
+
// overlapping query may re-deliver (e.g. an over-fetched log matched by two
|
|
1401
|
+
// partitions). Absorbs sorting too, so updateInternal doesn't re-sort.
|
|
1402
|
+
~mutItemsSorted=true,
|
|
1288
1403
|
~mutItems=?{
|
|
1289
1404
|
switch newItems {
|
|
1290
1405
|
| [] => None
|
|
1291
|
-
| _ => Some(fetchState.buffer->
|
|
1406
|
+
| _ => Some(fetchState.buffer->mergeIntoBuffer(newItems))
|
|
1292
1407
|
}
|
|
1293
1408
|
},
|
|
1294
1409
|
)
|
|
@@ -1308,7 +1423,8 @@ let startFetchingQueries = ({optimizedPartitions}: t, ~queries: array<query>) =>
|
|
|
1308
1423
|
fromBlock: q.fromBlock,
|
|
1309
1424
|
toBlock: q.toBlock,
|
|
1310
1425
|
isChunk: q.isChunk,
|
|
1311
|
-
|
|
1426
|
+
itemsTarget: q.itemsTarget,
|
|
1427
|
+
itemsEst: q.itemsEst,
|
|
1312
1428
|
fetchedBlock: None,
|
|
1313
1429
|
}
|
|
1314
1430
|
|
|
@@ -1329,163 +1445,191 @@ let startFetchingQueries = ({optimizedPartitions}: t, ~queries: array<query>) =>
|
|
|
1329
1445
|
}
|
|
1330
1446
|
}
|
|
1331
1447
|
|
|
1332
|
-
|
|
1333
|
-
let
|
|
1448
|
+
// Most parallel in-flight chunk queries a single partition may have at once.
|
|
1449
|
+
let maxPendingChunksPerPartition = 12
|
|
1450
|
+
|
|
1451
|
+
// Generates candidate queries for a gap range (a hole left between
|
|
1452
|
+
// completed/pending chunks, e.g. from an out-of-order partial response). Gaps
|
|
1453
|
+
// carry the range's low fromBlock, so the acceptance pass takes them before
|
|
1454
|
+
// forward progress. Priced by the partition's trusted density when it has one;
|
|
1455
|
+
// otherwise by the "available density" — the partition's equal-divide budget
|
|
1456
|
+
// spread over its remaining range this tick — so a small gap reserves
|
|
1457
|
+
// proportionally little instead of a noisy one-sample estimate. Chunks only on
|
|
1458
|
+
// a trusted POSITIVE density. Returns the created queries' total itemsEst.
|
|
1459
|
+
let pushGapFillQueries = (
|
|
1334
1460
|
queries: array<query>,
|
|
1335
1461
|
~partitionId: string,
|
|
1336
1462
|
~rangeFromBlock: int,
|
|
1337
1463
|
~rangeEndBlock: option<int>,
|
|
1338
|
-
~
|
|
1464
|
+
~knownHeight: int,
|
|
1465
|
+
~chainTargetBlock: int,
|
|
1339
1466
|
~maybeChunkRange: option<int>,
|
|
1467
|
+
~maxChunks: int,
|
|
1340
1468
|
~partition: partition,
|
|
1469
|
+
~partitionBudget: float,
|
|
1470
|
+
~chunkItemsMultiplier: float,
|
|
1341
1471
|
~selection: selection,
|
|
1342
1472
|
~addressesByContractName: dict<array<Address.t>>,
|
|
1343
|
-
~indexingAddresses: dict<indexingAddress>,
|
|
1344
1473
|
) => {
|
|
1345
|
-
|
|
1474
|
+
let cost = ref(0.)
|
|
1475
|
+
|
|
1476
|
+
// Gaps past the chain's target block wait: they regenerate from the
|
|
1477
|
+
// pending-walk each tick and fill once the target reaches them.
|
|
1478
|
+
if rangeFromBlock <= Pervasives.min(knownHeight, chainTargetBlock) && maxChunks > 0 {
|
|
1346
1479
|
switch rangeEndBlock {
|
|
1347
1480
|
| Some(endBlock) if rangeFromBlock > endBlock => ()
|
|
1348
1481
|
| _ =>
|
|
1349
|
-
|
|
1350
|
-
|
|
1482
|
+
let trustedDensity = partition->getTrustedDensity
|
|
1483
|
+
let maxBlock = switch rangeEndBlock {
|
|
1484
|
+
| Some(eb) => eb
|
|
1485
|
+
| None => chainTargetBlock
|
|
1486
|
+
}
|
|
1487
|
+
let pushSingleQuery = (~density, ~isChunk) => {
|
|
1488
|
+
let itemsTarget = densityItemsTarget(
|
|
1489
|
+
~density=density *. chunkItemsMultiplier,
|
|
1490
|
+
~fromBlock=rangeFromBlock,
|
|
1491
|
+
~toBlock=rangeEndBlock,
|
|
1492
|
+
~chainTargetBlock,
|
|
1493
|
+
)
|
|
1494
|
+
let itemsEst = densityItemsTarget(
|
|
1495
|
+
~density,
|
|
1496
|
+
~fromBlock=rangeFromBlock,
|
|
1497
|
+
~toBlock=rangeEndBlock,
|
|
1498
|
+
~chainTargetBlock,
|
|
1499
|
+
)
|
|
1351
1500
|
queries->Array.push({
|
|
1352
1501
|
partitionId,
|
|
1353
1502
|
fromBlock: rangeFromBlock,
|
|
1354
1503
|
toBlock: rangeEndBlock,
|
|
1355
1504
|
selection,
|
|
1356
|
-
isChunk
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
~fromBlock=rangeFromBlock,
|
|
1360
|
-
~toBlock=rangeEndBlock,
|
|
1361
|
-
~maxQueryBlockNumber,
|
|
1362
|
-
),
|
|
1363
|
-
chainId: 0,
|
|
1364
|
-
progress: 0.,
|
|
1505
|
+
isChunk,
|
|
1506
|
+
itemsTarget,
|
|
1507
|
+
itemsEst,
|
|
1365
1508
|
addressesByContractName,
|
|
1366
|
-
indexingAddresses,
|
|
1367
1509
|
})
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
}
|
|
1510
|
+
cost := cost.contents +. itemsEst->Int.toFloat
|
|
1511
|
+
}
|
|
1512
|
+
switch (trustedDensity, maybeChunkRange) {
|
|
1513
|
+
| (Some(density), Some(chunkRange)) if density > 0. =>
|
|
1373
1514
|
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 {
|
|
1515
|
+
if rangeFromBlock + chunkSize * 2 - 1 <= maxBlock {
|
|
1379
1516
|
let chunkFromBlock = ref(rangeFromBlock)
|
|
1380
1517
|
let chunkIdx = ref(0)
|
|
1381
1518
|
while (
|
|
1382
|
-
chunkIdx.contents <
|
|
1383
|
-
chunkFromBlock.contents + getChunkSize(chunkIdx.contents) - 1 <= maxBlock
|
|
1519
|
+
chunkIdx.contents < maxChunks && chunkFromBlock.contents + chunkSize - 1 <= maxBlock
|
|
1384
1520
|
) {
|
|
1385
|
-
let chunkToBlock = chunkFromBlock.contents +
|
|
1521
|
+
let chunkToBlock = chunkFromBlock.contents + chunkSize - 1
|
|
1522
|
+
let itemsTarget = densityItemsTarget(
|
|
1523
|
+
~density=density *. chunkItemsMultiplier,
|
|
1524
|
+
~fromBlock=chunkFromBlock.contents,
|
|
1525
|
+
~toBlock=Some(chunkToBlock),
|
|
1526
|
+
~chainTargetBlock,
|
|
1527
|
+
)
|
|
1528
|
+
let itemsEst = densityItemsTarget(
|
|
1529
|
+
~density,
|
|
1530
|
+
~fromBlock=chunkFromBlock.contents,
|
|
1531
|
+
~toBlock=Some(chunkToBlock),
|
|
1532
|
+
~chainTargetBlock,
|
|
1533
|
+
)
|
|
1386
1534
|
queries->Array.push({
|
|
1387
1535
|
partitionId,
|
|
1388
1536
|
fromBlock: chunkFromBlock.contents,
|
|
1389
1537
|
toBlock: Some(chunkToBlock),
|
|
1390
1538
|
isChunk: true,
|
|
1391
1539
|
selection,
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
~fromBlock=chunkFromBlock.contents,
|
|
1395
|
-
~toBlock=Some(chunkToBlock),
|
|
1396
|
-
~maxQueryBlockNumber,
|
|
1397
|
-
),
|
|
1398
|
-
chainId: 0,
|
|
1399
|
-
progress: 0.,
|
|
1540
|
+
itemsTarget,
|
|
1541
|
+
itemsEst,
|
|
1400
1542
|
addressesByContractName,
|
|
1401
|
-
indexingAddresses,
|
|
1402
1543
|
})
|
|
1544
|
+
cost := cost.contents +. itemsEst->Int.toFloat
|
|
1403
1545
|
chunkFromBlock := chunkToBlock + 1
|
|
1404
1546
|
chunkIdx := chunkIdx.contents + 1
|
|
1405
1547
|
}
|
|
1406
1548
|
} else {
|
|
1407
1549
|
// 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
|
-
})
|
|
1550
|
+
pushSingleQuery(~density, ~isChunk=rangeEndBlock !== None)
|
|
1425
1551
|
}
|
|
1552
|
+
| (Some(density), _) => pushSingleQuery(~density, ~isChunk=false)
|
|
1553
|
+
| (None, _) =>
|
|
1554
|
+
let remainingRange = Pervasives.max(1, chainTargetBlock - rangeFromBlock + 1)
|
|
1555
|
+
pushSingleQuery(~density=partitionBudget /. remainingRange->Int.toFloat, ~isChunk=false)
|
|
1426
1556
|
}
|
|
1427
1557
|
}
|
|
1428
1558
|
}
|
|
1559
|
+
cost.contents
|
|
1429
1560
|
}
|
|
1430
1561
|
|
|
1431
|
-
//
|
|
1432
|
-
|
|
1562
|
+
// Per-partition state carried from the gap-fill/cursor walk to candidate
|
|
1563
|
+
// generation.
|
|
1564
|
+
type partitionFillState = {
|
|
1565
|
+
partitionId: string,
|
|
1566
|
+
p: partition,
|
|
1567
|
+
cursor: int,
|
|
1568
|
+
// Chunks already generated for this partition during gap-fill — used with
|
|
1569
|
+
// pendingCount against maxPendingChunksPerPartition.
|
|
1570
|
+
chunksUsedThisCall: int,
|
|
1571
|
+
// Existing mutPendingQueries count before this call — fixed for the call.
|
|
1572
|
+
pendingCount: int,
|
|
1573
|
+
queryEndBlock: option<int>,
|
|
1574
|
+
maybeChunkRange: option<int>,
|
|
1575
|
+
}
|
|
1433
1576
|
|
|
1577
|
+
// Candidate queries are sized against chainTargetBlock, the soft querying
|
|
1578
|
+
// horizon the owning chain wants to reach this tick — derived by ChainState
|
|
1579
|
+
// from its share of the indexer-wide buffer budget and its chain-level event
|
|
1580
|
+
// density. chainTargetBlock is never used as a hard query end, only to (a)
|
|
1581
|
+
// select which partitions are "in range" this tick and (b) size an open-ended
|
|
1582
|
+
// query with no other ceiling: the true hard bounds stay
|
|
1583
|
+
// endBlock/mergeBlock/the lagged head.
|
|
1584
|
+
//
|
|
1585
|
+
// The tick's budget is chainTargetItems minus what's already in flight. Every
|
|
1586
|
+
// candidate query — gap-fill holes, plus each in-range partition's chunks or
|
|
1587
|
+
// open-ended probe toward the target — is generated with no budget check, then
|
|
1588
|
+
// the candidates are sorted by fromBlock and accepted in that order while the
|
|
1589
|
+
// budget stays positive. The query that tips it negative is still accepted (a
|
|
1590
|
+
// single overshoot); everything after it waits for a tick with more budget.
|
|
1591
|
+
// Sorting by fromBlock spends the budget on the earliest blocks across all
|
|
1592
|
+
// partitions first, so no partition is starved by generation order and the
|
|
1593
|
+
// frontier advances evenly. In-flight reservations release as responses land,
|
|
1594
|
+
// so acceptance redistributes across ticks.
|
|
1595
|
+
//
|
|
1596
|
+
// A partition with a trusted positive density (two or more responses — see
|
|
1597
|
+
// getMinHistoryRange) generates real, density-sized chunks toward the target.
|
|
1598
|
+
// Any other partition (no signal, one noisy sample, or a density-0 estimate)
|
|
1599
|
+
// generates one open-ended probe sized to the events its range to the target is
|
|
1600
|
+
// expected to hold — rangeTargetDensity × (chainTargetBlock − fromBlock + 1) /
|
|
1601
|
+
// inRangeCount — so several unknown-density partitions probe in parallel within
|
|
1602
|
+
// one budget, each scaled by how much of the range it still has to cover.
|
|
1434
1603
|
let getNextQuery = (
|
|
1435
|
-
{
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
blockLag,
|
|
1440
|
-
latestOnBlockBlockNumber,
|
|
1441
|
-
knownHeight,
|
|
1442
|
-
} as fetchState: t,
|
|
1443
|
-
~budget,
|
|
1444
|
-
~chainPendingBudget,
|
|
1604
|
+
{optimizedPartitions, blockLag, latestOnBlockBlockNumber, knownHeight, endBlock}: t,
|
|
1605
|
+
~chainTargetBlock: int,
|
|
1606
|
+
~chainTargetItems: float,
|
|
1607
|
+
~chunkItemsMultiplier: float=1.,
|
|
1445
1608
|
) => {
|
|
1446
1609
|
let headBlockNumber = knownHeight - blockLag
|
|
1447
1610
|
if headBlockNumber <= 0 {
|
|
1448
1611
|
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
1612
|
} else {
|
|
1454
1613
|
let isOnBlockBehindTheHead = latestOnBlockBlockNumber < headBlockNumber
|
|
1455
1614
|
let shouldWaitForNewBlock = ref(
|
|
1456
|
-
switch
|
|
1615
|
+
switch endBlock {
|
|
1457
1616
|
| Some(endBlock) => headBlockNumber < endBlock
|
|
1458
1617
|
| None => true
|
|
1459
1618
|
} &&
|
|
1460
1619
|
!isOnBlockBehindTheHead,
|
|
1461
1620
|
)
|
|
1462
1621
|
|
|
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
1622
|
let partitionsCount = optimizedPartitions.idsInAscOrder->Array.length
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1623
|
+
|
|
1624
|
+
// Every partition is visited once here regardless of whether it gets a
|
|
1625
|
+
// query pushed below — waiting-for-new-block bookkeeping shouldn't depend
|
|
1626
|
+
// on this tick's budget.
|
|
1627
|
+
for idx in 0 to partitionsCount - 1 {
|
|
1481
1628
|
let partitionId = optimizedPartitions.idsInAscOrder->Array.getUnsafe(idx)
|
|
1482
1629
|
let p = optimizedPartitions.entities->Dict.getUnsafe(partitionId)
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
let hasPendingQueries = pendingCount > 0
|
|
1487
|
-
|
|
1488
|
-
if hasPendingQueries || isBehindTheHead {
|
|
1630
|
+
if (
|
|
1631
|
+
p.mutPendingQueries->Array.length > 0 || p.latestFetchedBlock.blockNumber < headBlockNumber
|
|
1632
|
+
) {
|
|
1489
1633
|
// Even if there are some partitions waiting for the new block
|
|
1490
1634
|
// We still want to wait for all partitions reaching the head
|
|
1491
1635
|
// because they might update knownHeight in their response
|
|
@@ -1493,51 +1637,99 @@ let getNextQuery = (
|
|
|
1493
1637
|
// and we don't want to poll the head for a few small partitions
|
|
1494
1638
|
shouldWaitForNewBlock := false
|
|
1495
1639
|
}
|
|
1640
|
+
}
|
|
1496
1641
|
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1642
|
+
// One bucket per partition, in idsInAscOrder order — gap-fill and the
|
|
1643
|
+
// budget pass both push into a partition's own bucket, so flattening at
|
|
1644
|
+
// the end (see below) reproduces idsInAscOrder without a sort.
|
|
1645
|
+
let queriesByPartitionIndex: array<
|
|
1646
|
+
array<query>,
|
|
1647
|
+
> = Array.fromInitializer(~length=partitionsCount, _ => [])
|
|
1648
|
+
|
|
1649
|
+
// Compute queryEndBlock for this partition
|
|
1650
|
+
let computeQueryEndBlock = (p: partition) => {
|
|
1651
|
+
let queryEndBlock = Utils.Math.minOptInt(endBlock, p.mergeBlock)
|
|
1652
|
+
switch blockLag {
|
|
1502
1653
|
| 0 => queryEndBlock
|
|
1503
1654
|
| _ =>
|
|
1504
1655
|
// Force head block as an endBlock when blockLag is set
|
|
1505
1656
|
// because otherwise HyperSync might return bigger range
|
|
1506
1657
|
Utils.Math.minOptInt(Some(headBlockNumber), queryEndBlock)
|
|
1507
1658
|
}
|
|
1508
|
-
|
|
1509
|
-
// Otherwise for indexers with 100+ partitions
|
|
1510
|
-
// we might blow up the buffer size to more than 600k events
|
|
1511
|
-
// simply because of HyperSync returning extra blocks
|
|
1512
|
-
let queryEndBlock = switch (queryEndBlock, maxQueryBlockNumber < knownHeight) {
|
|
1513
|
-
| (Some(endBlock), true) => Some(Pervasives.min(maxQueryBlockNumber, endBlock))
|
|
1514
|
-
| (None, true) => Some(maxQueryBlockNumber)
|
|
1515
|
-
| (_, false) => queryEndBlock
|
|
1516
|
-
}
|
|
1659
|
+
}
|
|
1517
1660
|
|
|
1661
|
+
// In-flight itemsEst summed over queries still being fetched
|
|
1662
|
+
// (fetchedBlock === None). A query whose response already landed has had its
|
|
1663
|
+
// reservation released by ChainState even while it lingers in
|
|
1664
|
+
// mutPendingQueries behind an unfilled gap, so counting it here would
|
|
1665
|
+
// understate the budget. Sizes fresh forward work below.
|
|
1666
|
+
let chainReserved = ref(0.)
|
|
1667
|
+
|
|
1668
|
+
// (fromBlock, itemsEst) of each still-in-flight query. The acceptance pass
|
|
1669
|
+
// merges these into the candidate stream and draws them down in fromBlock
|
|
1670
|
+
// order, so a gap-fill sitting before an in-flight query claims budget ahead
|
|
1671
|
+
// of it and the buffer unblocks without waiting for that query to return.
|
|
1672
|
+
let reservations = []
|
|
1673
|
+
|
|
1674
|
+
// Position of each partition in idsInAscOrder, so an accepted query routes
|
|
1675
|
+
// back to its bucket and the output stays in idsInAscOrder. Filled in the
|
|
1676
|
+
// Phase A sweep below.
|
|
1677
|
+
let partitionIndexById = Dict.make()
|
|
1678
|
+
|
|
1679
|
+
// Every candidate query for this tick — gap-fill holes (Phase A) plus each
|
|
1680
|
+
// in-range partition's chunks/probe toward the target (Phase B) — generated
|
|
1681
|
+
// with no budget check, then merged with the in-flight reservations, sorted
|
|
1682
|
+
// by fromBlock, and accepted while the budget lasts (acceptance pass).
|
|
1683
|
+
// Selecting by fromBlock spends the budget on the earliest blocks across all
|
|
1684
|
+
// partitions first, so no partition is starved by iteration order and the
|
|
1685
|
+
// frontier advances evenly.
|
|
1686
|
+
let candidates = []
|
|
1687
|
+
|
|
1688
|
+
// Phase A: gap-fill. Walk each partition's pending queries once, generating
|
|
1689
|
+
// a candidate for any hole (e.g. from an out-of-order partial chunk
|
|
1690
|
+
// response). This also determines each partition's post-gap cursor and
|
|
1691
|
+
// whether it's blocked on an unresolved single-shot query.
|
|
1692
|
+
let fillStates = []
|
|
1693
|
+
for idx in 0 to partitionsCount - 1 {
|
|
1694
|
+
let partitionId = optimizedPartitions.idsInAscOrder->Array.getUnsafe(idx)
|
|
1695
|
+
let p = optimizedPartitions.entities->Dict.getUnsafe(partitionId)
|
|
1696
|
+
partitionIndexById->Dict.set(partitionId, idx)
|
|
1697
|
+
let pendingCount = p.mutPendingQueries->Array.length
|
|
1698
|
+
for pqIdx in 0 to pendingCount - 1 {
|
|
1699
|
+
let pq = p.mutPendingQueries->Array.getUnsafe(pqIdx)
|
|
1700
|
+
if pq.fetchedBlock === None {
|
|
1701
|
+
chainReserved := chainReserved.contents +. pq.itemsEst->Int.toFloat
|
|
1702
|
+
reservations->Array.push((pq.fromBlock, pq.itemsEst))->ignore
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
let queryEndBlock = computeQueryEndBlock(p)
|
|
1518
1706
|
let maybeChunkRange = getMinHistoryRange(p)
|
|
1519
1707
|
|
|
1520
|
-
// Walk pending queries to find open ranges and create queries for each
|
|
1521
1708
|
let cursor = ref(p.latestFetchedBlock.blockNumber + 1)
|
|
1522
1709
|
let canContinue = ref(true)
|
|
1710
|
+
let chunksUsedThisCall = ref(0)
|
|
1523
1711
|
let pqIdx = ref(0)
|
|
1524
|
-
while pqIdx.contents <
|
|
1712
|
+
while pqIdx.contents < pendingCount && canContinue.contents {
|
|
1525
1713
|
let pq = p.mutPendingQueries->Array.getUnsafe(pqIdx.contents)
|
|
1526
1714
|
|
|
1527
|
-
// Gap before this pending query → create queries for the gap range
|
|
1528
1715
|
if pq.fromBlock > cursor.contents {
|
|
1529
|
-
|
|
1530
|
-
|
|
1716
|
+
let beforeLen = candidates->Array.length
|
|
1717
|
+
pushGapFillQueries(
|
|
1718
|
+
candidates,
|
|
1531
1719
|
~partitionId,
|
|
1532
1720
|
~rangeFromBlock=cursor.contents,
|
|
1533
1721
|
~rangeEndBlock=Utils.Math.minOptInt(Some(pq.fromBlock - 1), queryEndBlock),
|
|
1534
|
-
~
|
|
1722
|
+
~knownHeight,
|
|
1723
|
+
~chainTargetBlock,
|
|
1535
1724
|
~maybeChunkRange,
|
|
1725
|
+
~maxChunks=maxPendingChunksPerPartition - pendingCount - chunksUsedThisCall.contents,
|
|
1536
1726
|
~partition=p,
|
|
1727
|
+
~partitionBudget=chainTargetItems /. partitionsCount->Int.toFloat,
|
|
1728
|
+
~chunkItemsMultiplier,
|
|
1537
1729
|
~selection=p.selection,
|
|
1538
1730
|
~addressesByContractName=p.addressesByContractName,
|
|
1539
|
-
|
|
1540
|
-
)
|
|
1731
|
+
)->ignore
|
|
1732
|
+
chunksUsedThisCall := chunksUsedThisCall.contents + (candidates->Array.length - beforeLen)
|
|
1541
1733
|
}
|
|
1542
1734
|
switch pq {
|
|
1543
1735
|
| {isChunk: true, toBlock: Some(toBlock), fetchedBlock: Some({blockNumber})}
|
|
@@ -1549,40 +1741,195 @@ let getNextQuery = (
|
|
|
1549
1741
|
pqIdx := pqIdx.contents + 1
|
|
1550
1742
|
}
|
|
1551
1743
|
|
|
1552
|
-
// Tail range after all pending queries
|
|
1553
1744
|
if canContinue.contents {
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
)
|
|
1745
|
+
fillStates
|
|
1746
|
+
->Array.push({
|
|
1747
|
+
partitionId,
|
|
1748
|
+
p,
|
|
1749
|
+
cursor: cursor.contents,
|
|
1750
|
+
chunksUsedThisCall: chunksUsedThisCall.contents,
|
|
1751
|
+
pendingCount,
|
|
1752
|
+
queryEndBlock,
|
|
1753
|
+
maybeChunkRange,
|
|
1754
|
+
})
|
|
1755
|
+
->ignore
|
|
1566
1756
|
}
|
|
1757
|
+
}
|
|
1567
1758
|
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1759
|
+
// Budget for fresh forward work: chainTargetItems minus what's still in
|
|
1760
|
+
// flight. Sizes probes and bounds chunk generation below; the acceptance
|
|
1761
|
+
// pass does the final budgeting against the full chainTargetItems.
|
|
1762
|
+
let freshBudget = Pervasives.max(0., chainTargetItems -. chainReserved.contents)
|
|
1763
|
+
|
|
1764
|
+
let isInRange = (fs: partitionFillState) =>
|
|
1765
|
+
fs.cursor <= chainTargetBlock &&
|
|
1766
|
+
switch fs.queryEndBlock {
|
|
1767
|
+
| Some(eb) => fs.cursor <= eb
|
|
1768
|
+
| None => true
|
|
1769
|
+
} &&
|
|
1770
|
+
fs.pendingCount + fs.chunksUsedThisCall < maxPendingChunksPerPartition
|
|
1771
|
+
|
|
1772
|
+
let inRangeStates = fillStates->Array.filter(isInRange)
|
|
1773
|
+
let inRangeCount = inRangeStates->Array.length
|
|
1774
|
+
// Even share of the fresh budget across the partitions actually fetching
|
|
1775
|
+
// this tick (not every partition — so budget isn't stranded on ones below
|
|
1776
|
+
// the head, waiting, or already done). The fallback when there's no range to
|
|
1777
|
+
// the target.
|
|
1778
|
+
let probeShare = inRangeCount == 0 ? 0. : freshBudget /. inRangeCount->Int.toFloat
|
|
1779
|
+
// Items/block the budget implies over the range those partitions cover this
|
|
1780
|
+
// tick — from the furthest-behind in-range cursor to the target. A probe
|
|
1781
|
+
// covering less of that range (its partition sits further ahead) gets
|
|
1782
|
+
// proportionally fewer items; one starting at the frontier gets the full
|
|
1783
|
+
// even share.
|
|
1784
|
+
let frontierCursor =
|
|
1785
|
+
inRangeStates->Array.reduce(chainTargetBlock, (min, fs) => fs.cursor < min ? fs.cursor : min)
|
|
1786
|
+
let rangeToTarget = chainTargetBlock - frontierCursor + 1
|
|
1787
|
+
let rangeTargetDensity =
|
|
1788
|
+
inRangeCount > 0 && rangeToTarget > 0 ? freshBudget /. rangeToTarget->Int.toFloat : 0.
|
|
1789
|
+
|
|
1790
|
+
// Phase B: generate each in-range partition's candidates — strict chunks
|
|
1791
|
+
// toward the target sized by real density (up to the pending-chunk cap), or,
|
|
1792
|
+
// for a partition without a trusted positive density, a single open-ended
|
|
1793
|
+
// probe at its even share of the budget. No budget check here; the
|
|
1794
|
+
// acceptance pass below decides which candidates make the cut.
|
|
1795
|
+
//
|
|
1796
|
+
// Chunks require a POSITIVE trusted density: density 0 prices every chunk at
|
|
1797
|
+
// ~nothing, so an open-ended probe (full server scan range in one response)
|
|
1798
|
+
// beats a pipeline of hard-bounded chunks that crawl 1.8× per two responses.
|
|
1799
|
+
inRangeStates->Array.forEach(fs => {
|
|
1800
|
+
let p = fs.p
|
|
1801
|
+
let maxBlock = switch fs.queryEndBlock {
|
|
1802
|
+
| Some(eb) => eb
|
|
1803
|
+
| None => chainTargetBlock
|
|
1804
|
+
}
|
|
1805
|
+
switch (fs.maybeChunkRange, p->getTrustedDensity) {
|
|
1806
|
+
| (Some(minHistoryRange), Some(density)) if density > 0. =>
|
|
1807
|
+
let chunkSize = Js.Math.ceil_int(minHistoryRange->Int.toFloat *. 1.8)
|
|
1808
|
+
let maxChunksRemaining =
|
|
1809
|
+
maxPendingChunksPerPartition - fs.pendingCount - fs.chunksUsedThisCall
|
|
1810
|
+
// No chunk starts past chainTargetBlock; an emitted chunk still keeps
|
|
1811
|
+
// its full span (chunkToBlock may exceed the target — only
|
|
1812
|
+
// endBlock/mergeBlock are hard bounds).
|
|
1813
|
+
let chunkStartCeiling = Pervasives.min(maxBlock, chainTargetBlock)
|
|
1814
|
+
let created = ref(0)
|
|
1815
|
+
let chunkFromBlock = ref(fs.cursor)
|
|
1816
|
+
// Stop once this partition alone has generated more than the whole fresh
|
|
1817
|
+
// budget: the acceptance pass can hand a single partition at most the
|
|
1818
|
+
// budget plus one overshoot query, so further chunks could never be
|
|
1819
|
+
// accepted. Bounds generation (and the candidate sort) when the budget
|
|
1820
|
+
// is small relative to the pending-chunk cap.
|
|
1821
|
+
let generatedItems = ref(0.)
|
|
1822
|
+
while (
|
|
1823
|
+
created.contents < maxChunksRemaining &&
|
|
1824
|
+
chunkFromBlock.contents <= chunkStartCeiling &&
|
|
1825
|
+
generatedItems.contents <= freshBudget
|
|
1826
|
+
) {
|
|
1827
|
+
let chunkToBlock = Pervasives.min(chunkFromBlock.contents + chunkSize - 1, maxBlock)
|
|
1828
|
+
let rawEst = density *. (chunkToBlock - chunkFromBlock.contents + 1)->Int.toFloat
|
|
1829
|
+
let itemsEst = Pervasives.max(1, rawEst->Math.ceil->Float.toInt)
|
|
1830
|
+
let itemsTarget = Pervasives.max(
|
|
1831
|
+
1,
|
|
1832
|
+
(rawEst *. chunkItemsMultiplier)->Math.ceil->Float.toInt,
|
|
1833
|
+
)
|
|
1834
|
+
candidates
|
|
1835
|
+
->Array.push({
|
|
1836
|
+
partitionId: fs.partitionId,
|
|
1837
|
+
fromBlock: chunkFromBlock.contents,
|
|
1838
|
+
toBlock: Some(chunkToBlock),
|
|
1839
|
+
isChunk: true,
|
|
1840
|
+
selection: p.selection,
|
|
1841
|
+
itemsTarget,
|
|
1842
|
+
itemsEst,
|
|
1843
|
+
addressesByContractName: p.addressesByContractName,
|
|
1844
|
+
})
|
|
1845
|
+
->ignore
|
|
1846
|
+
generatedItems := generatedItems.contents +. itemsEst->Int.toFloat
|
|
1847
|
+
chunkFromBlock := chunkToBlock + 1
|
|
1848
|
+
created := created.contents + 1
|
|
1849
|
+
}
|
|
1850
|
+
| _ =>
|
|
1851
|
+
// Size the probe by the events its range to the target is expected to
|
|
1852
|
+
// hold — rangeTargetDensity × (chainTargetBlock − fromBlock + 1), split
|
|
1853
|
+
// across the partitions fetching this tick. With no range to the target
|
|
1854
|
+
// fall back to an even share of the fresh budget, so cold chains and
|
|
1855
|
+
// caught-up partitions still probe.
|
|
1856
|
+
let itemsTarget = if rangeToTarget > 0 {
|
|
1857
|
+
Pervasives.max(
|
|
1858
|
+
1,
|
|
1859
|
+
Math.round(
|
|
1860
|
+
rangeTargetDensity *.
|
|
1861
|
+
(chainTargetBlock - fs.cursor + 1)->Int.toFloat /.
|
|
1862
|
+
inRangeCount->Int.toFloat,
|
|
1863
|
+
)->Float.toInt,
|
|
1864
|
+
)
|
|
1865
|
+
} else {
|
|
1866
|
+
Pervasives.max(1, Math.round(probeShare)->Float.toInt)
|
|
1867
|
+
}
|
|
1868
|
+
candidates
|
|
1869
|
+
->Array.push({
|
|
1870
|
+
partitionId: fs.partitionId,
|
|
1871
|
+
fromBlock: fs.cursor,
|
|
1872
|
+
toBlock: fs.queryEndBlock,
|
|
1873
|
+
isChunk: false,
|
|
1874
|
+
selection: p.selection,
|
|
1875
|
+
itemsTarget,
|
|
1876
|
+
itemsEst: itemsTarget,
|
|
1877
|
+
addressesByContractName: p.addressesByContractName,
|
|
1878
|
+
})
|
|
1580
1879
|
->ignore
|
|
1581
1880
|
}
|
|
1881
|
+
})
|
|
1582
1882
|
|
|
1583
|
-
|
|
1883
|
+
// Acceptance: merge fresh candidates (Some) with the in-flight reservations
|
|
1884
|
+
// (None) and walk them in fromBlock order, starting from the full
|
|
1885
|
+
// chainTargetItems. A reservation just draws down the budget — its query is
|
|
1886
|
+
// already sent — while a candidate draws down the budget and is emitted.
|
|
1887
|
+
// Because a gap-fill's fromBlock precedes the in-flight query it unblocks,
|
|
1888
|
+
// it claims budget ahead of that reservation, so the buffer never deadlocks
|
|
1889
|
+
// waiting on a hole it can't fund. The candidate that tips the budget
|
|
1890
|
+
// negative is still emitted (a single overshoot); everything after it waits
|
|
1891
|
+
// for a tick with more budget. Accepted queries route back to their
|
|
1892
|
+
// partition bucket, so the output stays in idsInAscOrder with each
|
|
1893
|
+
// partition's queries in fromBlock order.
|
|
1894
|
+
let acceptanceStream = []
|
|
1895
|
+
candidates->Array.forEach(query =>
|
|
1896
|
+
acceptanceStream->Array.push((query.fromBlock, query.itemsEst, Some(query)))->ignore
|
|
1897
|
+
)
|
|
1898
|
+
reservations->Array.forEach(((fromBlock, itemsEst)) =>
|
|
1899
|
+
acceptanceStream->Array.push((fromBlock, itemsEst, None))->ignore
|
|
1900
|
+
)
|
|
1901
|
+
// Sort by fromBlock; on a tie charge the in-flight reservation (None) before
|
|
1902
|
+
// a fresh candidate (Some), so a same-block candidate can't overshoot the
|
|
1903
|
+
// target buffer. Only a strictly-earlier candidate — a gap-fill, whose
|
|
1904
|
+
// fromBlock precedes the query it unblocks — borrows ahead of a reservation.
|
|
1905
|
+
acceptanceStream->Array.sort(((aFrom, _, aQuery), (bFrom, _, bQuery)) =>
|
|
1906
|
+
if aFrom !== bFrom {
|
|
1907
|
+
Int.compare(aFrom, bFrom)
|
|
1908
|
+
} else {
|
|
1909
|
+
switch (aQuery, bQuery) {
|
|
1910
|
+
| (None, Some(_)) => Ordering.less
|
|
1911
|
+
| (Some(_), None) => Ordering.greater
|
|
1912
|
+
| (None, None) | (Some(_), Some(_)) => Ordering.equal
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
)
|
|
1916
|
+
let streamCount = acceptanceStream->Array.length
|
|
1917
|
+
let remainingBudget = ref(chainTargetItems)
|
|
1918
|
+
let acceptIdx = ref(0)
|
|
1919
|
+
while remainingBudget.contents > 0. && acceptIdx.contents < streamCount {
|
|
1920
|
+
let (_, itemsEst, maybeQuery) = acceptanceStream->Array.getUnsafe(acceptIdx.contents)
|
|
1921
|
+
switch maybeQuery {
|
|
1922
|
+
| Some(query) =>
|
|
1923
|
+
let partitionIdx = partitionIndexById->Dict.getUnsafe(query.partitionId)
|
|
1924
|
+
queriesByPartitionIndex->Array.getUnsafe(partitionIdx)->Array.push(query)->ignore
|
|
1925
|
+
| None => ()
|
|
1926
|
+
}
|
|
1927
|
+
remainingBudget := remainingBudget.contents -. itemsEst->Int.toFloat
|
|
1928
|
+
acceptIdx := acceptIdx.contents + 1
|
|
1584
1929
|
}
|
|
1585
1930
|
|
|
1931
|
+
let queries = queriesByPartitionIndex->Array.flat
|
|
1932
|
+
|
|
1586
1933
|
if queries->Utils.Array.isEmpty {
|
|
1587
1934
|
if shouldWaitForNewBlock.contents {
|
|
1588
1935
|
WaitingForNewBlock
|
|
@@ -1631,14 +1978,15 @@ Instantiates a fetch state with partitions for initial addresses
|
|
|
1631
1978
|
let make = (
|
|
1632
1979
|
~startBlock,
|
|
1633
1980
|
~endBlock,
|
|
1634
|
-
~
|
|
1981
|
+
~onEventRegistrations: array<Internal.onEventRegistration>,
|
|
1982
|
+
~contractConfigs: dict<IndexingAddresses.contractConfig>,
|
|
1635
1983
|
~addresses: array<Internal.indexingAddress>,
|
|
1636
1984
|
~maxAddrInPartition,
|
|
1637
1985
|
~chainId,
|
|
1638
1986
|
~maxOnBlockBufferSize,
|
|
1639
1987
|
~knownHeight,
|
|
1640
1988
|
~progressBlockNumber=startBlock - 1,
|
|
1641
|
-
~
|
|
1989
|
+
~onBlockRegistrations=[],
|
|
1642
1990
|
~blockLag=0,
|
|
1643
1991
|
~firstEventBlock=None,
|
|
1644
1992
|
): t => {
|
|
@@ -1648,38 +1996,15 @@ let make = (
|
|
|
1648
1996
|
}
|
|
1649
1997
|
|
|
1650
1998
|
let notDependingOnAddresses = []
|
|
1651
|
-
let
|
|
1999
|
+
let normalRegistrations = []
|
|
1652
2000
|
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
2001
|
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
2002
|
+
onEventRegistrations->Array.forEach(reg => {
|
|
2003
|
+
if reg.dependsOnAddresses {
|
|
2004
|
+
normalRegistrations->Array.push(reg)
|
|
2005
|
+
contractNamesWithNormalEvents->Utils.Set.add(reg.eventConfig.contractName)->ignore
|
|
1681
2006
|
} else {
|
|
1682
|
-
notDependingOnAddresses->Array.push(
|
|
2007
|
+
notDependingOnAddresses->Array.push(reg)
|
|
1683
2008
|
}
|
|
1684
2009
|
})
|
|
1685
2010
|
|
|
@@ -1691,7 +2016,7 @@ let make = (
|
|
|
1691
2016
|
latestFetchedBlock,
|
|
1692
2017
|
selection: {
|
|
1693
2018
|
dependsOnAddresses: false,
|
|
1694
|
-
|
|
2019
|
+
onEventRegistrations: notDependingOnAddresses,
|
|
1695
2020
|
},
|
|
1696
2021
|
addressesByContractName: Dict.make(),
|
|
1697
2022
|
mergeBlock: None,
|
|
@@ -1706,7 +2031,7 @@ let make = (
|
|
|
1706
2031
|
|
|
1707
2032
|
let normalSelection = {
|
|
1708
2033
|
dependsOnAddresses: true,
|
|
1709
|
-
|
|
2034
|
+
onEventRegistrations: normalRegistrations,
|
|
1710
2035
|
}
|
|
1711
2036
|
|
|
1712
2037
|
let registeringContractsByContract: dict<dict<indexingAddress>> = Dict.make()
|
|
@@ -1714,32 +2039,16 @@ let make = (
|
|
|
1714
2039
|
|
|
1715
2040
|
addresses->Array.forEach(contract => {
|
|
1716
2041
|
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
2042
|
|
|
1737
2043
|
// Only addresses whose contract has events that depend on addresses get
|
|
1738
2044
|
// registered for active fetching via partitions.
|
|
1739
2045
|
if contractNamesWithNormalEvents->Utils.Set.has(contractName) {
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
2046
|
+
registeringContractsByContract
|
|
2047
|
+
->Utils.Dict.getOrInsertEmptyDict(contractName)
|
|
2048
|
+
->Dict.set(
|
|
2049
|
+
contract.address->Address.toString,
|
|
2050
|
+
IndexingAddresses.makeIndexingAddress(~contract, ~contractConfigs),
|
|
2051
|
+
)
|
|
1743
2052
|
|
|
1744
2053
|
// Detect dynamic contracts by registrationBlock
|
|
1745
2054
|
if contract.registrationBlock !== -1 {
|
|
@@ -1758,12 +2067,15 @@ let make = (
|
|
|
1758
2067
|
~progressBlockNumber,
|
|
1759
2068
|
)
|
|
1760
2069
|
|
|
1761
|
-
if
|
|
2070
|
+
if (
|
|
2071
|
+
optimizedPartitions->OptimizedPartitions.count === 0 &&
|
|
2072
|
+
onBlockRegistrations->Utils.Array.isEmpty
|
|
2073
|
+
) {
|
|
1762
2074
|
JsError.throwWithMessage(
|
|
1763
2075
|
`Invalid configuration: Nothing to fetch on chain ${chainId->Int.toString}. ` ++
|
|
1764
2076
|
`addresses=${addresses->Array.length->Int.toString}, ` ++
|
|
1765
|
-
`
|
|
1766
|
-
`
|
|
2077
|
+
`onEventRegistrations=${onEventRegistrations->Array.length->Int.toString}, ` ++
|
|
2078
|
+
`normalRegistrations=${normalRegistrations
|
|
1767
2079
|
->Array.length
|
|
1768
2080
|
->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
2081
|
)
|
|
@@ -1774,14 +2086,14 @@ let make = (
|
|
|
1774
2086
|
// fetching, so without seeding the buffer here getNextQuery would return
|
|
1775
2087
|
// NothingToQuery and the indexer would get stuck.
|
|
1776
2088
|
let buffer = []
|
|
1777
|
-
let latestOnBlockBlockNumber = if knownHeight > 0 &&
|
|
2089
|
+
let latestOnBlockBlockNumber = if knownHeight > 0 && onBlockRegistrations->Utils.Array.notEmpty {
|
|
1778
2090
|
let maxBlockNumber = switch optimizedPartitions->OptimizedPartitions.getLatestFullyFetchedBlock {
|
|
1779
2091
|
| None => knownHeight
|
|
1780
2092
|
| Some(latestFullyFetchedBlock) => latestFullyFetchedBlock.blockNumber
|
|
1781
2093
|
}
|
|
1782
2094
|
appendOnBlockItems(
|
|
1783
2095
|
~mutItems=buffer,
|
|
1784
|
-
~
|
|
2096
|
+
~onBlockRegistrations,
|
|
1785
2097
|
~indexerStartBlock=startBlock,
|
|
1786
2098
|
~fromBlock=progressBlockNumber,
|
|
1787
2099
|
~maxBlockNumber,
|
|
@@ -1799,17 +2111,14 @@ let make = (
|
|
|
1799
2111
|
endBlock,
|
|
1800
2112
|
latestOnBlockBlockNumber,
|
|
1801
2113
|
normalSelection,
|
|
1802
|
-
indexingAddresses,
|
|
1803
2114
|
blockLag,
|
|
1804
|
-
|
|
2115
|
+
onBlockRegistrations,
|
|
1805
2116
|
maxOnBlockBufferSize,
|
|
1806
2117
|
knownHeight,
|
|
1807
2118
|
buffer,
|
|
1808
2119
|
firstEventBlock,
|
|
1809
2120
|
}
|
|
1810
2121
|
|
|
1811
|
-
let numAddresses = indexingAddresses->Utils.Dict.size
|
|
1812
|
-
Prometheus.IndexingAddresses.set(~addressesCount=numAddresses, ~chainId)
|
|
1813
2122
|
Prometheus.IndexingPartitions.set(
|
|
1814
2123
|
~partitionsCount=optimizedPartitions->OptimizedPartitions.count,
|
|
1815
2124
|
~chainId,
|
|
@@ -1826,25 +2135,6 @@ let make = (
|
|
|
1826
2135
|
|
|
1827
2136
|
let bufferSize = ({buffer}: t) => buffer->Array.length
|
|
1828
2137
|
|
|
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
2138
|
let rollbackPendingQueries = (mutPendingQueries: array<pendingQuery>, ~targetBlockNumber) => {
|
|
1849
2139
|
// - Remove queries where fromBlock > target
|
|
1850
2140
|
// - Cap fetchedBlock at target where fetchedBlock > target
|
|
@@ -1876,18 +2166,11 @@ Always recreates optimized partitions to avoid duplicate addresses:
|
|
|
1876
2166
|
- Non-wildcard with lfb <= target: keep, adjust pending queries and mergeBlock
|
|
1877
2167
|
- Non-wildcard with lfb > target: delete, track addresses for recreation
|
|
1878
2168
|
*/
|
|
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
|
-
})
|
|
2169
|
+
let rollback = (fetchState: t, ~indexingAddresses: IndexingAddresses.t, ~targetBlockNumber) => {
|
|
2170
|
+
// Step 1: Prune addresses registered after the target block. The pruned index is
|
|
2171
|
+
// then the source of truth for partition cleanup below — an address survives iff
|
|
2172
|
+
// it's still in the index.
|
|
2173
|
+
indexingAddresses->IndexingAddresses.rollbackInPlace(~targetBlockNumber)
|
|
1891
2174
|
|
|
1892
2175
|
// Step 2: Categorize partitions
|
|
1893
2176
|
let keptPartitions = []
|
|
@@ -1917,18 +2200,12 @@ let rollback = (fetchState: t, ~targetBlockNumber) => {
|
|
|
1917
2200
|
| _ if p.latestFetchedBlock.blockNumber > targetBlockNumber =>
|
|
1918
2201
|
p.addressesByContractName->Utils.Dict.forEachWithKey((addresses, contractName) => {
|
|
1919
2202
|
addresses->Array.forEach(address => {
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
indexingAddresses
|
|
1923
|
-
->Utils.Dict.dangerouslyGetNonOption(address->Address.toString)
|
|
1924
|
-
->Option.isSome
|
|
1925
|
-
) {
|
|
2203
|
+
switch indexingAddresses->IndexingAddresses.get(address->Address.toString) {
|
|
2204
|
+
| Some(indexingContract) =>
|
|
1926
2205
|
let registeringContracts =
|
|
1927
2206
|
registeringContractsByContract->Utils.Dict.getOrInsertEmptyDict(contractName)
|
|
1928
|
-
registeringContracts->Dict.set(
|
|
1929
|
-
|
|
1930
|
-
indexingAddresses->Dict.getUnsafe(address->Address.toString),
|
|
1931
|
-
)
|
|
2207
|
+
registeringContracts->Dict.set(address->Address.toString, indexingContract)
|
|
2208
|
+
| None => ()
|
|
1932
2209
|
}
|
|
1933
2210
|
})
|
|
1934
2211
|
})
|
|
@@ -1941,11 +2218,13 @@ let rollback = (fetchState: t, ~targetBlockNumber) => {
|
|
|
1941
2218
|
| other => other
|
|
1942
2219
|
}
|
|
1943
2220
|
|
|
1944
|
-
//
|
|
2221
|
+
// Drop addresses pruned from the index
|
|
1945
2222
|
let rollbackedAddressesByContractName = Dict.make()
|
|
1946
2223
|
addressesByContractName->Utils.Dict.forEachWithKey((addresses, contractName) => {
|
|
1947
2224
|
let keptAddresses =
|
|
1948
|
-
addresses->Array.filter(address =>
|
|
2225
|
+
addresses->Array.filter(address =>
|
|
2226
|
+
indexingAddresses->IndexingAddresses.get(address->Address.toString)->Option.isSome
|
|
2227
|
+
)
|
|
1949
2228
|
if keptAddresses->Array.length > 0 {
|
|
1950
2229
|
rollbackedAddressesByContractName->Dict.set(contractName, keptAddresses)
|
|
1951
2230
|
}
|
|
@@ -1989,7 +2268,8 @@ let rollback = (fetchState: t, ~targetBlockNumber) => {
|
|
|
1989
2268
|
),
|
|
1990
2269
|
}->updateInternal(
|
|
1991
2270
|
~optimizedPartitions,
|
|
1992
|
-
|
|
2271
|
+
// Filtering the sorted buffer keeps it sorted and deduped.
|
|
2272
|
+
~mutItemsSorted=true,
|
|
1993
2273
|
~mutItems=fetchState.buffer->Array.filter(item =>
|
|
1994
2274
|
switch item {
|
|
1995
2275
|
| Event({blockNumber})
|
|
@@ -2084,17 +2364,6 @@ let getProgressPercentage = (fetchState: t) => {
|
|
|
2084
2364
|
}
|
|
2085
2365
|
}
|
|
2086
2366
|
|
|
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
2367
|
let sortForBatch = {
|
|
2099
2368
|
let hasFullBatch = ({buffer} as fetchState: t, ~batchSizeTarget) => {
|
|
2100
2369
|
switch buffer->Array.get(batchSizeTarget - 1) {
|