envio 3.3.0-alpha.10 → 3.3.0-alpha.12
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/licenses/CLA.md +35 -0
- package/licenses/EULA.md +67 -0
- package/licenses/LICENSE.md +45 -0
- package/licenses/README.md +35 -0
- package/package.json +9 -8
- package/src/Batch.res.mjs +1 -1
- package/src/ChainFetching.res +30 -18
- package/src/ChainFetching.res.mjs +23 -13
- package/src/ChainState.res +45 -30
- package/src/ChainState.res.mjs +22 -8
- package/src/ChainState.resi +5 -0
- package/src/Config.res +9 -5
- package/src/Config.res.mjs +2 -2
- package/src/Core.res +20 -0
- package/src/Core.res.mjs +12 -0
- package/src/CrossChainState.res +64 -29
- package/src/CrossChainState.res.mjs +17 -8
- package/src/EventConfigBuilder.res +21 -46
- package/src/EventConfigBuilder.res.mjs +18 -25
- package/src/EventProcessing.res +8 -5
- package/src/EventProcessing.res.mjs +2 -1
- package/src/FetchState.res +428 -322
- package/src/FetchState.res.mjs +316 -233
- package/src/HandlerRegister.res +3 -1
- package/src/HandlerRegister.res.mjs +3 -2
- package/src/Internal.res +11 -2
- package/src/LogSelection.res +0 -62
- package/src/LogSelection.res.mjs +0 -74
- package/src/RawEvent.res +2 -2
- package/src/Rollback.res +32 -13
- package/src/Rollback.res.mjs +24 -11
- package/src/SimulateDeadInputTracker.res +1 -1
- package/src/SimulateItems.res +38 -6
- package/src/SimulateItems.res.mjs +28 -9
- package/src/TestIndexer.res +2 -2
- package/src/TestIndexer.res.mjs +2 -2
- package/src/TopicFilter.res +1 -25
- package/src/TopicFilter.res.mjs +0 -74
- package/src/bindings/Viem.res +0 -5
- package/src/sources/EventRouter.res +0 -21
- package/src/sources/EventRouter.res.mjs +0 -12
- package/src/sources/EvmChain.res +2 -27
- package/src/sources/EvmChain.res.mjs +2 -23
- package/src/sources/EvmRpcClient.res +12 -15
- package/src/sources/EvmRpcClient.res.mjs +3 -3
- package/src/sources/HyperSync.res +9 -38
- package/src/sources/HyperSync.res.mjs +16 -28
- package/src/sources/HyperSync.resi +2 -2
- package/src/sources/HyperSyncClient.res +88 -11
- package/src/sources/HyperSyncClient.res.mjs +39 -6
- package/src/sources/HyperSyncSource.res +18 -199
- package/src/sources/HyperSyncSource.res.mjs +9 -128
- package/src/sources/Rpc.res +0 -32
- package/src/sources/Rpc.res.mjs +1 -46
- package/src/sources/RpcSource.res +29 -175
- package/src/sources/RpcSource.res.mjs +32 -110
- package/src/sources/SimulateSource.res +37 -19
- package/src/sources/SimulateSource.res.mjs +27 -10
- package/src/sources/SvmHyperSyncSource.res +13 -3
- package/src/sources/SvmHyperSyncSource.res.mjs +1 -1
package/src/FetchState.res
CHANGED
|
@@ -47,17 +47,19 @@ type partition = {
|
|
|
47
47
|
dynamicContract: option<string>,
|
|
48
48
|
// Mutable array for SourceManager sync - queries exist only while being fetched
|
|
49
49
|
mutPendingQueries: array<pendingQuery>,
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
|
|
50
|
+
// The last two ranges that measured how many blocks the source could return.
|
|
51
|
+
// Both must be non-zero before the minimum is trusted for chunk sizing.
|
|
52
|
+
sourceRangeCapacity: int,
|
|
53
|
+
prevSourceRangeCapacity: int,
|
|
54
|
+
// Smoothed items/block observed in responses. This is independent from the
|
|
55
|
+
// source's range capacity: even a response truncated by our own itemsTarget
|
|
56
|
+
// cap is useful density evidence while saying nothing about source capacity.
|
|
57
|
+
// None distinguishes a new partition from a real zero-density observation.
|
|
58
|
+
eventDensity: option<float>,
|
|
57
59
|
// Tracks the latestFetchedBlock.blockNumber of the most recent response
|
|
58
|
-
// that updated
|
|
59
|
-
// when parallel query responses arrive out of order.
|
|
60
|
-
|
|
60
|
+
// that updated sourceRangeCapacity. Prevents degradation of the chunking
|
|
61
|
+
// heuristic when parallel query responses arrive out of order.
|
|
62
|
+
latestSourceRangeCapacityUpdateBlock: int,
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
type query = {
|
|
@@ -111,22 +113,17 @@ let densityItemsTarget = (~density, ~fromBlock, ~toBlock, ~chainTargetBlock) =>
|
|
|
111
113
|
)
|
|
112
114
|
}
|
|
113
115
|
|
|
114
|
-
// Calculate the chunk range from
|
|
116
|
+
// Calculate the chunk range from the last two source-capacity observations.
|
|
115
117
|
let getMinHistoryRange = (p: partition) => {
|
|
116
|
-
switch (p.
|
|
118
|
+
switch (p.sourceRangeCapacity, p.prevSourceRangeCapacity) {
|
|
117
119
|
| (0, _) | (_, 0) => None
|
|
118
120
|
| (a, b) => Some(a < b ? a : b)
|
|
119
121
|
}
|
|
120
122
|
}
|
|
121
123
|
|
|
122
|
-
// Density
|
|
123
|
-
//
|
|
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
|
-
}
|
|
124
|
+
// Density has its own initialization signal and is useful independently from
|
|
125
|
+
// source-capacity history, including when an itemsTarget cap truncates a response.
|
|
126
|
+
let getTrustedDensity = (p: partition) => p.eventDensity
|
|
130
127
|
|
|
131
128
|
let getMinQueryRange = (partitions: array<partition>) => {
|
|
132
129
|
let min = ref(0)
|
|
@@ -134,8 +131,8 @@ let getMinQueryRange = (partitions: array<partition>) => {
|
|
|
134
131
|
let p = partitions->Array.getUnsafe(i)
|
|
135
132
|
|
|
136
133
|
// Even if it's fetching, set dynamicContract field
|
|
137
|
-
let a = p.
|
|
138
|
-
let b = p.
|
|
134
|
+
let a = p.sourceRangeCapacity
|
|
135
|
+
let b = p.prevSourceRangeCapacity
|
|
139
136
|
if a > 0 && (min.contents == 0 || a < min.contents) {
|
|
140
137
|
min := a
|
|
141
138
|
}
|
|
@@ -208,11 +205,14 @@ module OptimizedPartitions = {
|
|
|
208
205
|
nextPartitionIndexRef := nextPartitionIndexRef.contents + 1
|
|
209
206
|
let minRange = getMinQueryRange([p1, p2])
|
|
210
207
|
// The merged partition indexes both parents' addresses, so its expected
|
|
211
|
-
// event rate is the sum of their densities.
|
|
212
|
-
// density
|
|
213
|
-
//
|
|
214
|
-
let inheritedDensity =
|
|
215
|
-
|
|
208
|
+
// event rate is the sum of their densities. If neither parent has a
|
|
209
|
+
// trusted density, the merged partition probes for a fresh signal
|
|
210
|
+
// instead of treating zero as an observed density.
|
|
211
|
+
let inheritedDensity = switch (p1->getTrustedDensity, p2->getTrustedDensity) {
|
|
212
|
+
| (Some(p1Density), Some(p2Density)) => Some(p1Density +. p2Density)
|
|
213
|
+
| (Some(density), None) | (None, Some(density)) => Some(density)
|
|
214
|
+
| (None, None) => None
|
|
215
|
+
}
|
|
216
216
|
{
|
|
217
217
|
id: newId,
|
|
218
218
|
dynamicContract: Some(contractName),
|
|
@@ -221,10 +221,10 @@ module OptimizedPartitions = {
|
|
|
221
221
|
mergeBlock: None,
|
|
222
222
|
addressesByContractName: Dict.make(), // set below
|
|
223
223
|
mutPendingQueries: [],
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
224
|
+
sourceRangeCapacity: minRange,
|
|
225
|
+
prevSourceRangeCapacity: minRange,
|
|
226
|
+
eventDensity: inheritedDensity,
|
|
227
|
+
latestSourceRangeCapacityUpdateBlock: 0,
|
|
228
228
|
}
|
|
229
229
|
}
|
|
230
230
|
|
|
@@ -472,14 +472,28 @@ module OptimizedPartitions = {
|
|
|
472
472
|
pendingQuery.fetchedBlock = Some(latestFetchedBlock)
|
|
473
473
|
|
|
474
474
|
let blockRange = latestFetchedBlock.blockNumber - query.fromBlock + 1
|
|
475
|
-
//
|
|
475
|
+
// Update density for every response, independently from whether this range
|
|
476
|
+
// is valid evidence of source capacity. A cap hit is still useful density
|
|
477
|
+
// evidence because it reports items returned across the scanned range.
|
|
478
|
+
let observedEventDensity = itemsCount->Int.toFloat /. blockRange->Int.toFloat
|
|
479
|
+
// Seed from the first observation, then smooth every later observation
|
|
480
|
+
// with a 1:1 moving average. Keeping initialization explicit is important:
|
|
481
|
+
// zero is valid density evidence and must participate in the next blend.
|
|
482
|
+
let updatedEventDensity = switch p.eventDensity {
|
|
483
|
+
| None => Some(observedEventDensity)
|
|
484
|
+
| Some(eventDensity) => Some((eventDensity +. observedEventDensity) /. 2.)
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Skip updating source capacity if a later response already updated it.
|
|
476
488
|
// Prevents degradation of the chunking heuristic when parallel query
|
|
477
489
|
// responses arrive out of order (e.g. earlier query with smaller range
|
|
478
490
|
// arriving after a later query with bigger range).
|
|
479
|
-
let
|
|
480
|
-
latestFetchedBlock.blockNumber > p.
|
|
491
|
+
let shouldUpdateSourceRangeCapacity =
|
|
492
|
+
latestFetchedBlock.blockNumber > p.latestSourceRangeCapacityUpdateBlock &&
|
|
481
493
|
switch query.toBlock {
|
|
482
|
-
| None =>
|
|
494
|
+
| None =>
|
|
495
|
+
// Don't update source capacity when very close to the head.
|
|
496
|
+
latestFetchedBlock.blockNumber < knownHeight - 10
|
|
483
497
|
| Some(queryToBlock) =>
|
|
484
498
|
if latestFetchedBlock.blockNumber < queryToBlock {
|
|
485
499
|
// Partial response is direct capacity evidence — unless it was
|
|
@@ -497,9 +511,12 @@ module OptimizedPartitions = {
|
|
|
497
511
|
}
|
|
498
512
|
}
|
|
499
513
|
}
|
|
500
|
-
let
|
|
501
|
-
|
|
502
|
-
|
|
514
|
+
let updatedSourceRangeCapacity = shouldUpdateSourceRangeCapacity
|
|
515
|
+
? blockRange
|
|
516
|
+
: p.sourceRangeCapacity
|
|
517
|
+
let updatedPrevSourceRangeCapacity = shouldUpdateSourceRangeCapacity
|
|
518
|
+
? p.sourceRangeCapacity
|
|
519
|
+
: p.prevSourceRangeCapacity
|
|
503
520
|
|
|
504
521
|
// Process fetched queries from front of queue for main partition
|
|
505
522
|
let updatedLatestFetchedBlock = consumeFetchedQueries(
|
|
@@ -519,12 +536,12 @@ module OptimizedPartitions = {
|
|
|
519
536
|
let updatedMainPartition = {
|
|
520
537
|
...p,
|
|
521
538
|
latestFetchedBlock: updatedLatestFetchedBlock,
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
539
|
+
sourceRangeCapacity: updatedSourceRangeCapacity,
|
|
540
|
+
prevSourceRangeCapacity: updatedPrevSourceRangeCapacity,
|
|
541
|
+
eventDensity: updatedEventDensity,
|
|
542
|
+
latestSourceRangeCapacityUpdateBlock: shouldUpdateSourceRangeCapacity
|
|
526
543
|
? latestFetchedBlock.blockNumber
|
|
527
|
-
: p.
|
|
544
|
+
: p.latestSourceRangeCapacityUpdateBlock,
|
|
528
545
|
}
|
|
529
546
|
|
|
530
547
|
mutEntities->Dict.set(p.id, updatedMainPartition)
|
|
@@ -629,13 +646,90 @@ let bufferReadyCount = (fetchState: t) => {
|
|
|
629
646
|
/*
|
|
630
647
|
Comparitor for two events from the same chain. No need for chain id or timestamp
|
|
631
648
|
*/
|
|
632
|
-
let
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
649
|
+
let getRegistrationIndex = (item: Internal.item): int =>
|
|
650
|
+
switch item {
|
|
651
|
+
| Event({onEventRegistration}) => onEventRegistration.index
|
|
652
|
+
| Block({onBlockRegistration}) => onBlockRegistration.index
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Total order on buffer items: block, then logIndex, then registration index.
|
|
656
|
+
// Returns a plain int (-1/0/1) with explicit field comparisons so it can be
|
|
657
|
+
// called directly from the merge/insertion loops below — no Array.sort callback,
|
|
658
|
+
// no allocated key. `0` means a true duplicate: same log routed to the same
|
|
659
|
+
// registration (two registrations for one log differ by index and are kept).
|
|
660
|
+
let compareBufferItem = (a: Internal.item, b: Internal.item): int => {
|
|
661
|
+
let ba = a->Internal.getItemBlockNumber
|
|
662
|
+
let bb = b->Internal.getItemBlockNumber
|
|
663
|
+
if ba != bb {
|
|
664
|
+
ba < bb ? -1 : 1
|
|
636
665
|
} else {
|
|
637
|
-
|
|
666
|
+
let la = a->Internal.getItemLogIndex
|
|
667
|
+
let lb = b->Internal.getItemLogIndex
|
|
668
|
+
if la != lb {
|
|
669
|
+
la < lb ? -1 : 1
|
|
670
|
+
} else {
|
|
671
|
+
let ia = a->getRegistrationIndex
|
|
672
|
+
let ib = b->getRegistrationIndex
|
|
673
|
+
ia < ib ? -1 : ia > ib ? 1 : 0
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Merge a maybe-unsorted `newItems` run into the already-sorted, already-deduped
|
|
679
|
+
// `buffer`, dropping items equal on (blockNumber, logIndex, registration index).
|
|
680
|
+
// Single linear pass over both runs after ordering `newItems` in place; every
|
|
681
|
+
// comparison is a direct `compareBufferItem` call (V8 inlines it) rather than a
|
|
682
|
+
// callback through `Array.sort`.
|
|
683
|
+
let mergeIntoBuffer = (buffer: array<Internal.item>, newItems: array<Internal.item>): array<
|
|
684
|
+
Internal.item,
|
|
685
|
+
> => {
|
|
686
|
+
let n = newItems->Array.length
|
|
687
|
+
// Insertion sort: a source response is small and usually already ascending,
|
|
688
|
+
// so this is ~O(n) here.
|
|
689
|
+
for i in 1 to n - 1 {
|
|
690
|
+
let x = newItems->Array.getUnsafe(i)
|
|
691
|
+
let j = ref(i - 1)
|
|
692
|
+
while j.contents >= 0 && compareBufferItem(newItems->Array.getUnsafe(j.contents), x) > 0 {
|
|
693
|
+
newItems->Array.setUnsafe(j.contents + 1, newItems->Array.getUnsafe(j.contents))
|
|
694
|
+
j := j.contents - 1
|
|
695
|
+
}
|
|
696
|
+
newItems->Array.setUnsafe(j.contents + 1, x)
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
let m = buffer->Array.length
|
|
700
|
+
let merged = []
|
|
701
|
+
let last = ref(None)
|
|
702
|
+
let push = item =>
|
|
703
|
+
switch last.contents {
|
|
704
|
+
| Some(l) if compareBufferItem(l, item) === 0 => ()
|
|
705
|
+
| _ => {
|
|
706
|
+
merged->Array.push(item)
|
|
707
|
+
last := Some(item)
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
let i = ref(0)
|
|
712
|
+
let j = ref(0)
|
|
713
|
+
while i.contents < m && j.contents < n {
|
|
714
|
+
let a = buffer->Array.getUnsafe(i.contents)
|
|
715
|
+
let b = newItems->Array.getUnsafe(j.contents)
|
|
716
|
+
if compareBufferItem(a, b) <= 0 {
|
|
717
|
+
push(a)
|
|
718
|
+
i := i.contents + 1
|
|
719
|
+
} else {
|
|
720
|
+
push(b)
|
|
721
|
+
j := j.contents + 1
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
while i.contents < m {
|
|
725
|
+
push(buffer->Array.getUnsafe(i.contents))
|
|
726
|
+
i := i.contents + 1
|
|
727
|
+
}
|
|
728
|
+
while j.contents < n {
|
|
729
|
+
push(newItems->Array.getUnsafe(j.contents))
|
|
730
|
+
j := j.contents + 1
|
|
638
731
|
}
|
|
732
|
+
merged
|
|
639
733
|
}
|
|
640
734
|
|
|
641
735
|
// Some big number which should be bigger than any log index
|
|
@@ -706,48 +800,46 @@ let updateInternal = (
|
|
|
706
800
|
fetchState: t,
|
|
707
801
|
~optimizedPartitions=fetchState.optimizedPartitions,
|
|
708
802
|
~mutItems=?,
|
|
803
|
+
// Set when the caller already passes a sorted, deduped buffer (hot paths merge
|
|
804
|
+
// via mergeIntoBuffer or filter the sorted buffer). Otherwise mutItems is
|
|
805
|
+
// normalized here, so callers can hand over items in any order.
|
|
806
|
+
~mutItemsSorted=false,
|
|
709
807
|
~blockLag=fetchState.blockLag,
|
|
710
808
|
~knownHeight=fetchState.knownHeight,
|
|
711
809
|
): t => {
|
|
712
|
-
|
|
810
|
+
// The buffer to build on: the caller's items (normalized to sorted if needed),
|
|
811
|
+
// or the current buffer when only onBlock items change.
|
|
812
|
+
let base = switch mutItems {
|
|
813
|
+
| Some(items) => mutItemsSorted ? items : []->mergeIntoBuffer(items)
|
|
814
|
+
| None => fetchState.buffer
|
|
815
|
+
}
|
|
713
816
|
|
|
817
|
+
// onBlock items are generated as their own ascending (block, logIndex) run and
|
|
818
|
+
// folded into `base` by the single merge below.
|
|
819
|
+
let blockItems = []
|
|
714
820
|
let latestOnBlockBlockNumber = switch fetchState.onBlockRegistrations {
|
|
715
821
|
| [] => knownHeight
|
|
716
|
-
| onBlockRegistrations =>
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
|
726
|
-
|
|
|
727
|
-
}->Array.get(fetchState.maxOnBlockBufferSize - 1) {
|
|
728
|
-
| Some(item) => item->Internal.getItemBlockNumber
|
|
729
|
-
| None =>
|
|
730
|
-
switch optimizedPartitions->OptimizedPartitions.getLatestFullyFetchedBlock {
|
|
731
|
-
| None => knownHeight
|
|
732
|
-
| Some(latestFullyFetchedBlock) => latestFullyFetchedBlock.blockNumber
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
let mutItems = switch mutItemsRef.contents {
|
|
737
|
-
| Some(mutItems) => mutItems
|
|
738
|
-
| None => fetchState.buffer->Array.copy
|
|
822
|
+
| onBlockRegistrations =>
|
|
823
|
+
// Calculate the max block number we are going to create items for
|
|
824
|
+
// Use maxOnBlockBufferSize to get the last target item in the buffer
|
|
825
|
+
// (sorted, so this is the highest-block item within the buffer cap).
|
|
826
|
+
// All this needed to prevent OOM when adding too many block items to the queue
|
|
827
|
+
let maxBlockNumber = switch base->Array.get(fetchState.maxOnBlockBufferSize - 1) {
|
|
828
|
+
| Some(item) => item->Internal.getItemBlockNumber
|
|
829
|
+
| None =>
|
|
830
|
+
switch optimizedPartitions->OptimizedPartitions.getLatestFullyFetchedBlock {
|
|
831
|
+
| None => knownHeight
|
|
832
|
+
| Some(latestFullyFetchedBlock) => latestFullyFetchedBlock.blockNumber
|
|
739
833
|
}
|
|
740
|
-
mutItemsRef := Some(mutItems)
|
|
741
|
-
|
|
742
|
-
appendOnBlockItems(
|
|
743
|
-
~mutItems,
|
|
744
|
-
~onBlockRegistrations,
|
|
745
|
-
~indexerStartBlock=fetchState.startBlock,
|
|
746
|
-
~fromBlock=fetchState.latestOnBlockBlockNumber,
|
|
747
|
-
~maxBlockNumber,
|
|
748
|
-
~maxOnBlockBufferSize=fetchState.maxOnBlockBufferSize,
|
|
749
|
-
)
|
|
750
834
|
}
|
|
835
|
+
appendOnBlockItems(
|
|
836
|
+
~mutItems=blockItems,
|
|
837
|
+
~onBlockRegistrations,
|
|
838
|
+
~indexerStartBlock=fetchState.startBlock,
|
|
839
|
+
~fromBlock=fetchState.latestOnBlockBlockNumber,
|
|
840
|
+
~maxBlockNumber,
|
|
841
|
+
~maxOnBlockBufferSize=fetchState.maxOnBlockBufferSize,
|
|
842
|
+
)
|
|
751
843
|
}
|
|
752
844
|
|
|
753
845
|
let updatedFetchState = {
|
|
@@ -762,15 +854,10 @@ let updateInternal = (
|
|
|
762
854
|
latestOnBlockBlockNumber,
|
|
763
855
|
blockLag,
|
|
764
856
|
knownHeight,
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
| Some(mutItems) => {
|
|
770
|
-
mutItems->Array.sort(compareBufferItem)
|
|
771
|
-
mutItems
|
|
772
|
-
}
|
|
773
|
-
| None => fetchState.buffer
|
|
857
|
+
// Single merge point: fold any onBlock items into the sorted base buffer.
|
|
858
|
+
buffer: switch blockItems {
|
|
859
|
+
| [] => base
|
|
860
|
+
| blockItems => base->mergeIntoBuffer(blockItems)
|
|
774
861
|
},
|
|
775
862
|
firstEventBlock: fetchState.firstEventBlock,
|
|
776
863
|
}
|
|
@@ -916,10 +1003,10 @@ OptimizedPartitions.t => {
|
|
|
916
1003
|
addressesByContractName,
|
|
917
1004
|
mergeBlock: None,
|
|
918
1005
|
mutPendingQueries: [],
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
1006
|
+
sourceRangeCapacity: 0,
|
|
1007
|
+
prevSourceRangeCapacity: 0,
|
|
1008
|
+
eventDensity: None,
|
|
1009
|
+
latestSourceRangeCapacityUpdateBlock: 0,
|
|
923
1010
|
})
|
|
924
1011
|
nextPartitionIndexRef := nextPartitionIndexRef.contents + 1
|
|
925
1012
|
}
|
|
@@ -1252,10 +1339,10 @@ let registerDynamicContracts = (
|
|
|
1252
1339
|
addressesByContractName,
|
|
1253
1340
|
mergeBlock: None,
|
|
1254
1341
|
mutPendingQueries: p.mutPendingQueries,
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1342
|
+
sourceRangeCapacity: p.sourceRangeCapacity,
|
|
1343
|
+
prevSourceRangeCapacity: p.prevSourceRangeCapacity,
|
|
1344
|
+
eventDensity: p.eventDensity,
|
|
1345
|
+
latestSourceRangeCapacityUpdateBlock: p.latestSourceRangeCapacityUpdateBlock,
|
|
1259
1346
|
})
|
|
1260
1347
|
}
|
|
1261
1348
|
}
|
|
@@ -1287,6 +1374,26 @@ let registerDynamicContracts = (
|
|
|
1287
1374
|
}
|
|
1288
1375
|
}
|
|
1289
1376
|
|
|
1377
|
+
// Drop events an address-param filter rejects. A merged partition may
|
|
1378
|
+
// over-fetch a wildcard event whose indexed address param references an
|
|
1379
|
+
// address registered after the log's block; `clientAddressFilter` is the
|
|
1380
|
+
// param-level analogue of EventRouter's srcAddress effectiveStartBlock check.
|
|
1381
|
+
let filterByClientAddress = (
|
|
1382
|
+
items: array<Internal.item>,
|
|
1383
|
+
~indexingAddresses: IndexingAddresses.t,
|
|
1384
|
+
): array<Internal.item> =>
|
|
1385
|
+
items->Array.filter(item =>
|
|
1386
|
+
switch item {
|
|
1387
|
+
| Internal.Event({payload, blockNumber}) as item =>
|
|
1388
|
+
switch (item->Internal.castUnsafeEventItem).onEventRegistration.clientAddressFilter {
|
|
1389
|
+
| Some(filter) =>
|
|
1390
|
+
filter(payload, blockNumber, indexingAddresses->IndexingAddresses.rawForFilter)
|
|
1391
|
+
| None => true
|
|
1392
|
+
}
|
|
1393
|
+
| _ => true
|
|
1394
|
+
}
|
|
1395
|
+
)
|
|
1396
|
+
|
|
1290
1397
|
/*
|
|
1291
1398
|
Updates fetchState with a response for a given query.
|
|
1292
1399
|
Returns Error if the partition with given query cannot be found (unexpected)
|
|
@@ -1295,26 +1402,10 @@ newItems are ordered earliest to latest (as they are returned from the worker)
|
|
|
1295
1402
|
*/
|
|
1296
1403
|
let handleQueryResult = (
|
|
1297
1404
|
fetchState: t,
|
|
1298
|
-
~indexingAddresses: IndexingAddresses.t,
|
|
1299
1405
|
~query: query,
|
|
1300
1406
|
~latestFetchedBlock: blockNumberAndTimestamp,
|
|
1301
1407
|
~newItems,
|
|
1302
1408
|
): t => {
|
|
1303
|
-
// Drop events an address-param filter rejects. A merged partition may
|
|
1304
|
-
// over-fetch a wildcard event whose indexed address param references an
|
|
1305
|
-
// address registered after the log's block; `clientAddressFilter` is the
|
|
1306
|
-
// param-level analogue of EventRouter's srcAddress effectiveStartBlock check.
|
|
1307
|
-
let newItems = newItems->Array.filter(item =>
|
|
1308
|
-
switch item {
|
|
1309
|
-
| Internal.Event({onEventRegistration, payload, blockNumber}) =>
|
|
1310
|
-
switch onEventRegistration.clientAddressFilter {
|
|
1311
|
-
| Some(filter) =>
|
|
1312
|
-
filter(payload, blockNumber, indexingAddresses->IndexingAddresses.rawForFilter)
|
|
1313
|
-
| None => true
|
|
1314
|
-
}
|
|
1315
|
-
| _ => true
|
|
1316
|
-
}
|
|
1317
|
-
)
|
|
1318
1409
|
fetchState->updateInternal(
|
|
1319
1410
|
~optimizedPartitions=fetchState.optimizedPartitions->OptimizedPartitions.handleQueryResponse(
|
|
1320
1411
|
~query,
|
|
@@ -1322,10 +1413,14 @@ let handleQueryResult = (
|
|
|
1322
1413
|
~itemsCount=newItems->Array.length,
|
|
1323
1414
|
~latestFetchedBlock,
|
|
1324
1415
|
),
|
|
1416
|
+
// Merge the response into the sorted buffer, dropping duplicates an
|
|
1417
|
+
// overlapping query may re-deliver (e.g. an over-fetched log matched by two
|
|
1418
|
+
// partitions). Absorbs sorting too, so updateInternal doesn't re-sort.
|
|
1419
|
+
~mutItemsSorted=true,
|
|
1325
1420
|
~mutItems=?{
|
|
1326
1421
|
switch newItems {
|
|
1327
1422
|
| [] => None
|
|
1328
|
-
| _ => Some(fetchState.buffer->
|
|
1423
|
+
| _ => Some(fetchState.buffer->mergeIntoBuffer(newItems))
|
|
1329
1424
|
}
|
|
1330
1425
|
},
|
|
1331
1426
|
)
|
|
@@ -1370,14 +1465,14 @@ let startFetchingQueries = ({optimizedPartitions}: t, ~queries: array<query>) =>
|
|
|
1370
1465
|
// Most parallel in-flight chunk queries a single partition may have at once.
|
|
1371
1466
|
let maxPendingChunksPerPartition = 12
|
|
1372
1467
|
|
|
1373
|
-
//
|
|
1374
|
-
// out-of-order partial response)
|
|
1375
|
-
// range
|
|
1376
|
-
// partition's trusted density when it has one;
|
|
1377
|
-
// density" — the partition's equal-divide budget
|
|
1378
|
-
// range this tick — so a small gap reserves
|
|
1379
|
-
// noisy one-sample estimate. Chunks only on
|
|
1380
|
-
//
|
|
1468
|
+
// Generates candidate queries for a gap range (a hole left between
|
|
1469
|
+
// completed/pending chunks, e.g. from an out-of-order partial response). Gaps
|
|
1470
|
+
// carry the range's low fromBlock, so the acceptance pass takes them before
|
|
1471
|
+
// forward progress. Priced by the partition's trusted density when it has one;
|
|
1472
|
+
// otherwise by the "available density" — the partition's equal-divide budget
|
|
1473
|
+
// spread over its remaining range this tick — so a small gap reserves
|
|
1474
|
+
// proportionally little instead of a noisy one-sample estimate. Chunks only on
|
|
1475
|
+
// a trusted POSITIVE density. Returns the created queries' total itemsEst.
|
|
1381
1476
|
let pushGapFillQueries = (
|
|
1382
1477
|
queries: array<query>,
|
|
1383
1478
|
~partitionId: string,
|
|
@@ -1481,46 +1576,19 @@ let pushGapFillQueries = (
|
|
|
1481
1576
|
cost.contents
|
|
1482
1577
|
}
|
|
1483
1578
|
|
|
1484
|
-
//
|
|
1485
|
-
//
|
|
1486
|
-
|
|
1487
|
-
// are topped up exactly to L, so the pour equals the budget no matter how
|
|
1488
|
-
// uneven the footprints are.
|
|
1489
|
-
let waterLevel = (~budget: float, ~footprints: array<float>) => {
|
|
1490
|
-
let sorted = footprints->Array.toSorted(Float.compare)
|
|
1491
|
-
let n = sorted->Array.length
|
|
1492
|
-
let prefix = ref(0.)
|
|
1493
|
-
let level = ref(None)
|
|
1494
|
-
let idx = ref(0)
|
|
1495
|
-
while level.contents == None && idx.contents < n {
|
|
1496
|
-
let i = idx.contents
|
|
1497
|
-
prefix := prefix.contents +. sorted->Array.getUnsafe(i)
|
|
1498
|
-
// The level if only the i+1 lowest footprints receive water — correct once
|
|
1499
|
-
// it doesn't reach the next footprint up.
|
|
1500
|
-
let candidate = (budget +. prefix.contents) /. (i + 1)->Int.toFloat
|
|
1501
|
-
if i == n - 1 || candidate <= sorted->Array.getUnsafe(i + 1) {
|
|
1502
|
-
level := Some(candidate)
|
|
1503
|
-
}
|
|
1504
|
-
idx := idx.contents + 1
|
|
1505
|
-
}
|
|
1506
|
-
level.contents->Option.getOr(0.)
|
|
1507
|
-
}
|
|
1508
|
-
|
|
1509
|
-
// Per-partition mutable state threaded through the water-fill rounds below.
|
|
1510
|
-
type waterFillState = {
|
|
1579
|
+
// Per-partition state carried from the gap-fill/cursor walk to candidate
|
|
1580
|
+
// generation.
|
|
1581
|
+
type partitionFillState = {
|
|
1511
1582
|
partitionId: string,
|
|
1512
1583
|
p: partition,
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
//
|
|
1516
|
-
|
|
1584
|
+
cursor: int,
|
|
1585
|
+
// Chunks already generated for this partition during gap-fill — used with
|
|
1586
|
+
// pendingCount against maxPendingChunksPerPartition.
|
|
1587
|
+
chunksUsedThisCall: int,
|
|
1588
|
+
// Existing mutPendingQueries count before this call — fixed for the call.
|
|
1517
1589
|
pendingCount: int,
|
|
1518
1590
|
queryEndBlock: option<int>,
|
|
1519
1591
|
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
1592
|
}
|
|
1525
1593
|
|
|
1526
1594
|
// Candidate queries are sized against chainTargetBlock, the soft querying
|
|
@@ -1531,24 +1599,25 @@ type waterFillState = {
|
|
|
1531
1599
|
// query with no other ceiling: the true hard bounds stay
|
|
1532
1600
|
// endBlock/mergeBlock/the lagged head.
|
|
1533
1601
|
//
|
|
1534
|
-
//
|
|
1535
|
-
//
|
|
1536
|
-
//
|
|
1537
|
-
//
|
|
1538
|
-
//
|
|
1539
|
-
//
|
|
1540
|
-
//
|
|
1541
|
-
//
|
|
1542
|
-
//
|
|
1602
|
+
// The tick's budget is chainTargetItems minus what's already in flight. A
|
|
1603
|
+
// non-positive budget only resolves the wait action and generates no query
|
|
1604
|
+
// candidates. With a positive budget, every candidate query — gap-fill holes,
|
|
1605
|
+
// plus each in-range partition's chunks or open-ended probe toward the target —
|
|
1606
|
+
// is generated with no budget check, then the candidates are sorted by
|
|
1607
|
+
// fromBlock and accepted in that order while the budget stays positive. The
|
|
1608
|
+
// query that tips it negative is still accepted (a single overshoot);
|
|
1609
|
+
// everything after it waits for a tick with more budget.
|
|
1610
|
+
// Sorting by fromBlock spends the budget on the earliest blocks across all
|
|
1611
|
+
// partitions first, so no partition is starved by generation order and the
|
|
1612
|
+
// frontier advances evenly. In-flight reservations release as responses land,
|
|
1613
|
+
// so acceptance redistributes across ticks.
|
|
1543
1614
|
//
|
|
1544
|
-
// A partition with a
|
|
1545
|
-
//
|
|
1546
|
-
//
|
|
1547
|
-
//
|
|
1548
|
-
//
|
|
1549
|
-
//
|
|
1550
|
-
// density-0 estimate) emits one open-ended probe sized exactly at its
|
|
1551
|
-
// allotment instead.
|
|
1615
|
+
// A partition with source-capacity history and a positive density generates
|
|
1616
|
+
// density-sized chunks toward the target. Any other partition (no signal, no
|
|
1617
|
+
// capacity history, or a density-0 estimate) generates one open-ended probe
|
|
1618
|
+
// sized to the events its range to the target is expected to hold —
|
|
1619
|
+
// rangeTargetDensity × (chainTargetBlock − fromBlock + 1) / inRangeCount — so
|
|
1620
|
+
// unknown-density partitions probe in parallel within one budget.
|
|
1552
1621
|
let getNextQuery = (
|
|
1553
1622
|
{optimizedPartitions, blockLag, latestOnBlockBlockNumber, knownHeight, endBlock}: t,
|
|
1554
1623
|
~chainTargetBlock: int,
|
|
@@ -1588,8 +1657,14 @@ let getNextQuery = (
|
|
|
1588
1657
|
}
|
|
1589
1658
|
}
|
|
1590
1659
|
|
|
1591
|
-
//
|
|
1592
|
-
//
|
|
1660
|
+
// A zero budget is an admission check: preserve the wait-state scan above,
|
|
1661
|
+
// but make every query-generation pass below empty. Caught-up chains also
|
|
1662
|
+
// skip those passes because their action is already known.
|
|
1663
|
+
let partitionsCount =
|
|
1664
|
+
chainTargetItems <= 0. || shouldWaitForNewBlock.contents ? 0 : partitionsCount
|
|
1665
|
+
|
|
1666
|
+
// One bucket per partition, in idsInAscOrder order — gap-fill and the
|
|
1667
|
+
// budget pass both push into a partition's own bucket, so flattening at
|
|
1593
1668
|
// the end (see below) reproduces idsInAscOrder without a sort.
|
|
1594
1669
|
let queriesByPartitionIndex: array<
|
|
1595
1670
|
array<query>,
|
|
@@ -1607,36 +1682,50 @@ let getNextQuery = (
|
|
|
1607
1682
|
}
|
|
1608
1683
|
}
|
|
1609
1684
|
|
|
1610
|
-
//
|
|
1611
|
-
//
|
|
1612
|
-
//
|
|
1613
|
-
//
|
|
1614
|
-
//
|
|
1615
|
-
let existingReservedByPartition = Dict.make()
|
|
1685
|
+
// In-flight itemsEst summed over queries still being fetched
|
|
1686
|
+
// (fetchedBlock === None). A query whose response already landed has had its
|
|
1687
|
+
// reservation released by ChainState even while it lingers in
|
|
1688
|
+
// mutPendingQueries behind an unfilled gap, so counting it here would
|
|
1689
|
+
// understate the budget. Sizes fresh forward work below.
|
|
1616
1690
|
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
|
|
1623
|
-
}
|
|
1624
|
-
existingReservedByPartition->Dict.set(partitionId, cost.contents)
|
|
1625
|
-
chainReserved := chainReserved.contents +. cost.contents
|
|
1626
|
-
}
|
|
1627
1691
|
|
|
1628
|
-
//
|
|
1629
|
-
//
|
|
1630
|
-
//
|
|
1631
|
-
//
|
|
1692
|
+
// (fromBlock, itemsEst) of each still-in-flight query. The acceptance pass
|
|
1693
|
+
// merges these into the candidate stream and draws them down in fromBlock
|
|
1694
|
+
// order, so a gap-fill sitting before an in-flight query claims budget ahead
|
|
1695
|
+
// of it and the buffer unblocks without waiting for that query to return.
|
|
1696
|
+
let reservations = []
|
|
1697
|
+
|
|
1698
|
+
// Position of each partition in idsInAscOrder, so an accepted query routes
|
|
1699
|
+
// back to its bucket and the output stays in idsInAscOrder. Filled in the
|
|
1700
|
+
// Phase A sweep below.
|
|
1701
|
+
let partitionIndexById = Dict.make()
|
|
1702
|
+
|
|
1703
|
+
// Every candidate query for this tick — gap-fill holes (Phase A) plus each
|
|
1704
|
+
// in-range partition's chunks/probe toward the target (Phase B) — generated
|
|
1705
|
+
// with no budget check, then merged with the in-flight reservations, sorted
|
|
1706
|
+
// by fromBlock, and accepted while the budget lasts (acceptance pass).
|
|
1707
|
+
// Selecting by fromBlock spends the budget on the earliest blocks across all
|
|
1708
|
+
// partitions first, so no partition is starved by iteration order and the
|
|
1709
|
+
// frontier advances evenly.
|
|
1710
|
+
let candidates = []
|
|
1711
|
+
|
|
1712
|
+
// Phase A: gap-fill. Walk each partition's pending queries once, generating
|
|
1713
|
+
// a candidate for any hole (e.g. from an out-of-order partial chunk
|
|
1714
|
+
// response). This also determines each partition's post-gap cursor and
|
|
1632
1715
|
// whether it's blocked on an unresolved single-shot query.
|
|
1633
1716
|
let fillStates = []
|
|
1634
|
-
let gapFillCost = ref(0.)
|
|
1635
1717
|
for idx in 0 to partitionsCount - 1 {
|
|
1636
1718
|
let partitionId = optimizedPartitions.idsInAscOrder->Array.getUnsafe(idx)
|
|
1637
1719
|
let p = optimizedPartitions.entities->Dict.getUnsafe(partitionId)
|
|
1638
|
-
|
|
1720
|
+
partitionIndexById->Dict.set(partitionId, idx)
|
|
1639
1721
|
let pendingCount = p.mutPendingQueries->Array.length
|
|
1722
|
+
for pqIdx in 0 to pendingCount - 1 {
|
|
1723
|
+
let pq = p.mutPendingQueries->Array.getUnsafe(pqIdx)
|
|
1724
|
+
if pq.fetchedBlock === None {
|
|
1725
|
+
chainReserved := chainReserved.contents +. pq.itemsEst->Int.toFloat
|
|
1726
|
+
reservations->Array.push((pq.fromBlock, pq.itemsEst))->ignore
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1640
1729
|
let queryEndBlock = computeQueryEndBlock(p)
|
|
1641
1730
|
let maybeChunkRange = getMinHistoryRange(p)
|
|
1642
1731
|
|
|
@@ -1648,9 +1737,9 @@ let getNextQuery = (
|
|
|
1648
1737
|
let pq = p.mutPendingQueries->Array.getUnsafe(pqIdx.contents)
|
|
1649
1738
|
|
|
1650
1739
|
if pq.fromBlock > cursor.contents {
|
|
1651
|
-
let beforeLen =
|
|
1652
|
-
|
|
1653
|
-
|
|
1740
|
+
let beforeLen = candidates->Array.length
|
|
1741
|
+
pushGapFillQueries(
|
|
1742
|
+
candidates,
|
|
1654
1743
|
~partitionId,
|
|
1655
1744
|
~rangeFromBlock=cursor.contents,
|
|
1656
1745
|
~rangeEndBlock=Utils.Math.minOptInt(Some(pq.fromBlock - 1), queryEndBlock),
|
|
@@ -1663,9 +1752,8 @@ let getNextQuery = (
|
|
|
1663
1752
|
~chunkItemsMultiplier,
|
|
1664
1753
|
~selection=p.selection,
|
|
1665
1754
|
~addressesByContractName=p.addressesByContractName,
|
|
1666
|
-
)
|
|
1667
|
-
chunksUsedThisCall := chunksUsedThisCall.contents + (
|
|
1668
|
-
gapFillCost := gapFillCost.contents +. cost
|
|
1755
|
+
)->ignore
|
|
1756
|
+
chunksUsedThisCall := chunksUsedThisCall.contents + (candidates->Array.length - beforeLen)
|
|
1669
1757
|
}
|
|
1670
1758
|
switch pq {
|
|
1671
1759
|
| {isChunk: true, toBlock: Some(toBlock), fetchedBlock: Some({blockNumber})}
|
|
@@ -1687,35 +1775,17 @@ let getNextQuery = (
|
|
|
1687
1775
|
pendingCount,
|
|
1688
1776
|
queryEndBlock,
|
|
1689
1777
|
maybeChunkRange,
|
|
1690
|
-
bucket,
|
|
1691
1778
|
})
|
|
1692
1779
|
->ignore
|
|
1693
1780
|
}
|
|
1694
1781
|
}
|
|
1695
1782
|
|
|
1696
|
-
//
|
|
1697
|
-
//
|
|
1698
|
-
//
|
|
1699
|
-
|
|
1700
|
-
let rangeItemsTarget = Pervasives.max(
|
|
1701
|
-
0.,
|
|
1702
|
-
chainTargetItems -. chainReserved.contents -. gapFillCost.contents,
|
|
1703
|
-
)
|
|
1783
|
+
// Budget for fresh forward work: chainTargetItems minus what's still in
|
|
1784
|
+
// flight. Sizes probes and bounds chunk generation below; the acceptance
|
|
1785
|
+
// pass does the final budgeting against the full chainTargetItems.
|
|
1786
|
+
let freshBudget = Pervasives.max(0., chainTargetItems -. chainReserved.contents)
|
|
1704
1787
|
|
|
1705
|
-
|
|
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
|
|
1714
|
-
}
|
|
1715
|
-
reservedByPartition->Dict.set(fs.partitionId, cost.contents)
|
|
1716
|
-
})
|
|
1717
|
-
|
|
1718
|
-
let isInRange = (fs: waterFillState) =>
|
|
1788
|
+
let isInRange = (fs: partitionFillState) =>
|
|
1719
1789
|
fs.cursor <= chainTargetBlock &&
|
|
1720
1790
|
switch fs.queryEndBlock {
|
|
1721
1791
|
| Some(eb) => fs.cursor <= eb
|
|
@@ -1724,16 +1794,32 @@ let getNextQuery = (
|
|
|
1724
1794
|
fs.pendingCount + fs.chunksUsedThisCall < maxPendingChunksPerPartition
|
|
1725
1795
|
|
|
1726
1796
|
let inRangeStates = fillStates->Array.filter(isInRange)
|
|
1727
|
-
|
|
1728
|
-
//
|
|
1729
|
-
//
|
|
1730
|
-
//
|
|
1797
|
+
let inRangeCount = inRangeStates->Array.length
|
|
1798
|
+
// Even share of the fresh budget across the partitions actually fetching
|
|
1799
|
+
// this tick (not every partition — so budget isn't stranded on ones below
|
|
1800
|
+
// the head, waiting, or already done). The fallback when there's no range to
|
|
1801
|
+
// the target.
|
|
1802
|
+
let probeShare = inRangeCount == 0 ? 0. : freshBudget /. inRangeCount->Int.toFloat
|
|
1803
|
+
// Items/block the budget implies over the range those partitions cover this
|
|
1804
|
+
// tick — from the furthest-behind in-range cursor to the target. A probe
|
|
1805
|
+
// covering less of that range (its partition sits further ahead) gets
|
|
1806
|
+
// proportionally fewer items; one starting at the frontier gets the full
|
|
1807
|
+
// even share.
|
|
1808
|
+
let frontierCursor =
|
|
1809
|
+
inRangeStates->Array.reduce(chainTargetBlock, (min, fs) => fs.cursor < min ? fs.cursor : min)
|
|
1810
|
+
let rangeToTarget = chainTargetBlock - frontierCursor + 1
|
|
1811
|
+
let rangeTargetDensity =
|
|
1812
|
+
inRangeCount > 0 && rangeToTarget > 0 ? freshBudget /. rangeToTarget->Int.toFloat : 0.
|
|
1813
|
+
|
|
1814
|
+
// Phase B: generate each in-range partition's candidates — strict chunks
|
|
1815
|
+
// when both source-capacity history and density are known, or an open-ended
|
|
1816
|
+
// budget probe otherwise. No budget check here; the acceptance pass below
|
|
1817
|
+
// decides which candidates make the cut.
|
|
1731
1818
|
//
|
|
1732
|
-
// Chunks require a POSITIVE trusted density: density 0 prices every chunk
|
|
1733
|
-
//
|
|
1734
|
-
// hard-bounded
|
|
1735
|
-
|
|
1736
|
-
let emitQueries = (fs: waterFillState, ~budget: float) => {
|
|
1819
|
+
// Chunks require a POSITIVE trusted density: density 0 prices every chunk at
|
|
1820
|
+
// ~nothing, so an open-ended probe (full server scan range in one response)
|
|
1821
|
+
// beats a pipeline of hard-bounded chunks that crawl 1.8× per two responses.
|
|
1822
|
+
inRangeStates->Array.forEach(fs => {
|
|
1737
1823
|
let p = fs.p
|
|
1738
1824
|
let maxBlock = switch fs.queryEndBlock {
|
|
1739
1825
|
| Some(eb) => eb
|
|
@@ -1741,23 +1827,25 @@ let getNextQuery = (
|
|
|
1741
1827
|
}
|
|
1742
1828
|
switch (fs.maybeChunkRange, p->getTrustedDensity) {
|
|
1743
1829
|
| (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
1830
|
let chunkSize = Js.Math.ceil_int(minHistoryRange->Int.toFloat *. 1.8)
|
|
1748
1831
|
let maxChunksRemaining =
|
|
1749
1832
|
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
1833
|
// No chunk starts past chainTargetBlock; an emitted chunk still keeps
|
|
1755
1834
|
// its full span (chunkToBlock may exceed the target — only
|
|
1756
1835
|
// endBlock/mergeBlock are hard bounds).
|
|
1836
|
+
let chunkStartCeiling = Pervasives.min(maxBlock, chainTargetBlock)
|
|
1837
|
+
let created = ref(0)
|
|
1838
|
+
let chunkFromBlock = ref(fs.cursor)
|
|
1839
|
+
// Stop once this partition alone has generated more than the whole fresh
|
|
1840
|
+
// budget: the acceptance pass can hand a single partition at most the
|
|
1841
|
+
// budget plus one overshoot query, so further chunks could never be
|
|
1842
|
+
// accepted. Bounds generation (and the candidate sort) when the budget
|
|
1843
|
+
// is small relative to the pending-chunk cap.
|
|
1844
|
+
let generatedItems = ref(0.)
|
|
1757
1845
|
while (
|
|
1758
|
-
budgetLeft.contents &&
|
|
1759
1846
|
created.contents < maxChunksRemaining &&
|
|
1760
|
-
chunkFromBlock.contents <=
|
|
1847
|
+
chunkFromBlock.contents <= chunkStartCeiling &&
|
|
1848
|
+
generatedItems.contents <= freshBudget
|
|
1761
1849
|
) {
|
|
1762
1850
|
let chunkToBlock = Pervasives.min(chunkFromBlock.contents + chunkSize - 1, maxBlock)
|
|
1763
1851
|
let rawEst = density *. (chunkToBlock - chunkFromBlock.contents + 1)->Int.toFloat
|
|
@@ -1766,89 +1854,103 @@ let getNextQuery = (
|
|
|
1766
1854
|
1,
|
|
1767
1855
|
(rawEst *. chunkItemsMultiplier)->Math.ceil->Float.toInt,
|
|
1768
1856
|
)
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
budgetLeft := false
|
|
1785
|
-
}
|
|
1857
|
+
candidates
|
|
1858
|
+
->Array.push({
|
|
1859
|
+
partitionId: fs.partitionId,
|
|
1860
|
+
fromBlock: chunkFromBlock.contents,
|
|
1861
|
+
toBlock: Some(chunkToBlock),
|
|
1862
|
+
isChunk: true,
|
|
1863
|
+
selection: p.selection,
|
|
1864
|
+
itemsTarget,
|
|
1865
|
+
itemsEst,
|
|
1866
|
+
addressesByContractName: p.addressesByContractName,
|
|
1867
|
+
})
|
|
1868
|
+
->ignore
|
|
1869
|
+
generatedItems := generatedItems.contents +. itemsEst->Int.toFloat
|
|
1870
|
+
chunkFromBlock := chunkToBlock + 1
|
|
1871
|
+
created := created.contents + 1
|
|
1786
1872
|
}
|
|
1787
|
-
fs.cursor = chunkFromBlock.contents
|
|
1788
|
-
fs.chunksUsedThisCall = fs.chunksUsedThisCall + created.contents
|
|
1789
|
-
consumed.contents
|
|
1790
1873
|
| _ =>
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1874
|
+
// Size the probe by the events its range to the target is expected to
|
|
1875
|
+
// hold — rangeTargetDensity × (chainTargetBlock − fromBlock + 1), split
|
|
1876
|
+
// across the partitions fetching this tick. With no range to the target
|
|
1877
|
+
// fall back to an even share of the fresh budget, so cold chains and
|
|
1878
|
+
// caught-up partitions still probe.
|
|
1879
|
+
let itemsTarget = if rangeToTarget > 0 {
|
|
1880
|
+
Pervasives.max(
|
|
1881
|
+
1,
|
|
1882
|
+
Math.round(
|
|
1883
|
+
rangeTargetDensity *.
|
|
1884
|
+
(chainTargetBlock - fs.cursor + 1)->Int.toFloat /.
|
|
1885
|
+
inRangeCount->Int.toFloat,
|
|
1886
|
+
)->Float.toInt,
|
|
1887
|
+
)
|
|
1888
|
+
} else {
|
|
1889
|
+
Pervasives.max(1, Math.round(probeShare)->Float.toInt)
|
|
1890
|
+
}
|
|
1891
|
+
candidates
|
|
1892
|
+
->Array.push({
|
|
1794
1893
|
partitionId: fs.partitionId,
|
|
1795
1894
|
fromBlock: fs.cursor,
|
|
1796
1895
|
toBlock: fs.queryEndBlock,
|
|
1797
1896
|
isChunk: false,
|
|
1798
1897
|
selection: p.selection,
|
|
1799
1898
|
itemsTarget,
|
|
1800
|
-
itemsEst,
|
|
1899
|
+
itemsEst: itemsTarget,
|
|
1801
1900
|
addressesByContractName: p.addressesByContractName,
|
|
1802
1901
|
})
|
|
1803
|
-
|
|
1804
|
-
fs.chunksUsedThisCall = fs.chunksUsedThisCall + 1
|
|
1805
|
-
itemsEst->Int.toFloat
|
|
1902
|
+
->ignore
|
|
1806
1903
|
}
|
|
1807
|
-
}
|
|
1904
|
+
})
|
|
1808
1905
|
|
|
1809
|
-
//
|
|
1810
|
-
//
|
|
1811
|
-
//
|
|
1812
|
-
//
|
|
1813
|
-
//
|
|
1814
|
-
//
|
|
1815
|
-
//
|
|
1816
|
-
//
|
|
1817
|
-
//
|
|
1818
|
-
//
|
|
1819
|
-
//
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
if fs->isInRange {
|
|
1841
|
-
next->Array.push(fs)->ignore
|
|
1842
|
-
}
|
|
1906
|
+
// Acceptance: merge fresh candidates (Some) with the in-flight reservations
|
|
1907
|
+
// (None) and walk them in fromBlock order, starting from the full
|
|
1908
|
+
// chainTargetItems. A reservation just draws down the budget — its query is
|
|
1909
|
+
// already sent — while a candidate draws down the budget and is emitted.
|
|
1910
|
+
// Because a gap-fill's fromBlock precedes the in-flight query it unblocks,
|
|
1911
|
+
// it claims budget ahead of that reservation, so the buffer never deadlocks
|
|
1912
|
+
// waiting on a hole it can't fund. The candidate that tips the budget
|
|
1913
|
+
// negative is still emitted (a single overshoot); everything after it waits
|
|
1914
|
+
// for a tick with more budget. Accepted queries route back to their
|
|
1915
|
+
// partition bucket, so the output stays in idsInAscOrder with each
|
|
1916
|
+
// partition's queries in fromBlock order.
|
|
1917
|
+
let acceptanceStream = []
|
|
1918
|
+
candidates->Array.forEach(query =>
|
|
1919
|
+
acceptanceStream->Array.push((query.fromBlock, query.itemsEst, Some(query)))->ignore
|
|
1920
|
+
)
|
|
1921
|
+
reservations->Array.forEach(((fromBlock, itemsEst)) =>
|
|
1922
|
+
acceptanceStream->Array.push((fromBlock, itemsEst, None))->ignore
|
|
1923
|
+
)
|
|
1924
|
+
// Sort by fromBlock; on a tie charge the in-flight reservation (None) before
|
|
1925
|
+
// a fresh candidate (Some), so a same-block candidate can't overshoot the
|
|
1926
|
+
// target buffer. Only a strictly-earlier candidate — a gap-fill, whose
|
|
1927
|
+
// fromBlock precedes the query it unblocks — borrows ahead of a reservation.
|
|
1928
|
+
acceptanceStream->Array.sort(((aFrom, _, aQuery), (bFrom, _, bQuery)) =>
|
|
1929
|
+
if aFrom !== bFrom {
|
|
1930
|
+
Int.compare(aFrom, bFrom)
|
|
1931
|
+
} else {
|
|
1932
|
+
switch (aQuery, bQuery) {
|
|
1933
|
+
| (None, Some(_)) => Ordering.less
|
|
1934
|
+
| (Some(_), None) => Ordering.greater
|
|
1935
|
+
| (None, None) | (Some(_), Some(_)) => Ordering.equal
|
|
1843
1936
|
}
|
|
1844
|
-
}
|
|
1845
|
-
|
|
1937
|
+
}
|
|
1938
|
+
)
|
|
1939
|
+
let streamCount = acceptanceStream->Array.length
|
|
1940
|
+
let remainingBudget = ref(chainTargetItems)
|
|
1941
|
+
let acceptIdx = ref(0)
|
|
1942
|
+
while remainingBudget.contents > 0. && acceptIdx.contents < streamCount {
|
|
1943
|
+
let (_, itemsEst, maybeQuery) = acceptanceStream->Array.getUnsafe(acceptIdx.contents)
|
|
1944
|
+
switch maybeQuery {
|
|
1945
|
+
| Some(query) =>
|
|
1946
|
+
let partitionIdx = partitionIndexById->Dict.getUnsafe(query.partitionId)
|
|
1947
|
+
queriesByPartitionIndex->Array.getUnsafe(partitionIdx)->Array.push(query)->ignore
|
|
1948
|
+
| None => ()
|
|
1949
|
+
}
|
|
1950
|
+
remainingBudget := remainingBudget.contents -. itemsEst->Int.toFloat
|
|
1951
|
+
acceptIdx := acceptIdx.contents + 1
|
|
1846
1952
|
}
|
|
1847
1953
|
|
|
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
1954
|
let queries = queriesByPartitionIndex->Array.flat
|
|
1853
1955
|
|
|
1854
1956
|
if queries->Utils.Array.isEmpty {
|
|
@@ -1943,10 +2045,10 @@ let make = (
|
|
|
1943
2045
|
mergeBlock: None,
|
|
1944
2046
|
dynamicContract: None,
|
|
1945
2047
|
mutPendingQueries: [],
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
2048
|
+
sourceRangeCapacity: 0,
|
|
2049
|
+
prevSourceRangeCapacity: 0,
|
|
2050
|
+
eventDensity: None,
|
|
2051
|
+
latestSourceRangeCapacityUpdateBlock: 0,
|
|
1950
2052
|
})
|
|
1951
2053
|
}
|
|
1952
2054
|
|
|
@@ -2189,6 +2291,8 @@ let rollback = (fetchState: t, ~indexingAddresses: IndexingAddresses.t, ~targetB
|
|
|
2189
2291
|
),
|
|
2190
2292
|
}->updateInternal(
|
|
2191
2293
|
~optimizedPartitions,
|
|
2294
|
+
// Filtering the sorted buffer keeps it sorted and deduped.
|
|
2295
|
+
~mutItemsSorted=true,
|
|
2192
2296
|
~mutItems=fetchState.buffer->Array.filter(item =>
|
|
2193
2297
|
switch item {
|
|
2194
2298
|
| Event({blockNumber})
|
|
@@ -2264,13 +2368,15 @@ let isReadyToEnterReorgThreshold = ({endBlock, blockLag, buffer, knownHeight} as
|
|
|
2264
2368
|
buffer->Utils.Array.isEmpty
|
|
2265
2369
|
}
|
|
2266
2370
|
|
|
2267
|
-
// Lower progress percentage = further behind = higher priority.
|
|
2268
|
-
//
|
|
2371
|
+
// Lower progress percentage = further behind = higher priority. Progress is
|
|
2372
|
+
// relative to the head this chain can actually fetch, so a chain at its lagged
|
|
2373
|
+
// head does not look behind relative to unavailable blocks. Shared by the batch
|
|
2374
|
+
// ordering and the cross-chain fetch priority.
|
|
2269
2375
|
let getProgressPercentage = (fetchState: t) => {
|
|
2270
2376
|
switch fetchState.firstEventBlock {
|
|
2271
2377
|
| None => 0.
|
|
2272
2378
|
| Some(firstEventBlock) =>
|
|
2273
|
-
let totalRange = fetchState.knownHeight - firstEventBlock
|
|
2379
|
+
let totalRange = fetchState.knownHeight - fetchState.blockLag - firstEventBlock
|
|
2274
2380
|
if totalRange <= 0 {
|
|
2275
2381
|
0.
|
|
2276
2382
|
} else {
|