envio 3.3.0-alpha.9 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +19 -0
- 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/BatchProcessing.res +0 -7
- package/src/BatchProcessing.res.mjs +1 -8
- package/src/ChainFetching.res +30 -18
- package/src/ChainFetching.res.mjs +23 -13
- package/src/ChainState.res +134 -55
- package/src/ChainState.res.mjs +78 -36
- package/src/ChainState.resi +14 -2
- 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 +115 -34
- package/src/CrossChainState.res.mjs +37 -14
- package/src/EffectState.res +100 -0
- package/src/EffectState.res.mjs +74 -0
- package/src/EffectState.resi +32 -0
- package/src/Envio.res +25 -7
- package/src/Envio.res.mjs +15 -19
- 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 +814 -467
- package/src/FetchState.res.mjs +541 -369
- package/src/HandlerRegister.res +3 -1
- package/src/HandlerRegister.res.mjs +3 -2
- package/src/InMemoryStore.res +14 -26
- package/src/InMemoryStore.res.mjs +6 -19
- package/src/IndexerState.res +15 -39
- package/src/IndexerState.res.mjs +18 -29
- package/src/IndexerState.resi +2 -10
- package/src/Internal.res +98 -14
- package/src/Internal.res.mjs +96 -2
- package/src/LoadLayer.res +44 -24
- package/src/LoadLayer.res.mjs +43 -20
- package/src/LoadLayer.resi +1 -0
- package/src/LogSelection.res +0 -62
- package/src/LogSelection.res.mjs +0 -74
- package/src/Metrics.res +1 -1
- package/src/Metrics.res.mjs +1 -1
- package/src/Persistence.res +12 -3
- package/src/PgStorage.res +155 -82
- package/src/PgStorage.res.mjs +130 -77
- package/src/Prometheus.res +20 -10
- package/src/Prometheus.res.mjs +22 -10
- package/src/PruneStaleHistory.res +146 -35
- package/src/PruneStaleHistory.res.mjs +114 -20
- 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/UserContext.res +62 -23
- package/src/UserContext.res.mjs +26 -6
- package/src/Writing.res +60 -21
- package/src/Writing.res.mjs +27 -9
- package/src/bindings/NodeJs.res +5 -0
- 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 +58 -23
- package/src/sources/EvmRpcClient.res.mjs +11 -5
- 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 +129 -522
- package/src/sources/RpcSource.res.mjs +161 -366
- package/src/sources/SimulateSource.res +37 -19
- package/src/sources/SimulateSource.res.mjs +27 -10
- package/src/sources/SourceManager.res +3 -7
- package/src/sources/SourceManager.res.mjs +2 -7
- package/src/sources/SourceManager.resi +0 -2
- package/src/sources/SvmHyperSyncSource.res +13 -3
- package/src/sources/SvmHyperSyncSource.res.mjs +1 -1
package/src/FetchState.res
CHANGED
|
@@ -16,10 +16,12 @@ type pendingQuery = {
|
|
|
16
16
|
fromBlock: int,
|
|
17
17
|
toBlock: option<int>,
|
|
18
18
|
isChunk: bool,
|
|
19
|
-
// Items this in-flight query is targeting (server maxNumLogs-style cap)
|
|
20
|
-
// from the query so the shared buffer budget can account for what's already
|
|
21
|
-
// being fetched.
|
|
19
|
+
// Items this in-flight query is targeting (server maxNumLogs-style cap).
|
|
22
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,
|
|
23
25
|
// Stores latestFetchedBlock when query completes. Only needed to persist
|
|
24
26
|
// timestamp while earlier queries are still pending before updating
|
|
25
27
|
// the partition's latestFetchedBlock.
|
|
@@ -43,19 +45,26 @@ type partition = {
|
|
|
43
45
|
// When set, partition indexes a single dynamic contract type.
|
|
44
46
|
// The addressesByContractName must contain only addresses for this contract.
|
|
45
47
|
dynamicContract: option<string>,
|
|
46
|
-
//
|
|
48
|
+
// Mutated in place and shared across fetchState versions (updateInternal
|
|
49
|
+
// copies the record, not this array): startFetchingQueries inserts,
|
|
50
|
+
// handleQueryResponse marks fetched and consumes, and the same array object
|
|
51
|
+
// survives every copy. Invariant: any path that invalidates the fetch
|
|
52
|
+
// frontier (reorg/rollback) must go through resetPendingQueries, otherwise a
|
|
53
|
+
// stale version's in-flight bookkeeping leaks into the restored state.
|
|
47
54
|
mutPendingQueries: array<pendingQuery>,
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
+
// The last two ranges that measured how many blocks the source could return.
|
|
56
|
+
// Both must be non-zero before the minimum is trusted for chunk sizing.
|
|
57
|
+
sourceRangeCapacity: int,
|
|
58
|
+
prevSourceRangeCapacity: int,
|
|
59
|
+
// Smoothed items/block observed in responses. This is independent from the
|
|
60
|
+
// source's range capacity: even a response truncated by our own itemsTarget
|
|
61
|
+
// cap is useful density evidence while saying nothing about source capacity.
|
|
62
|
+
// None distinguishes a new partition from a real zero-density observation.
|
|
63
|
+
eventDensity: option<float>,
|
|
55
64
|
// Tracks the latestFetchedBlock.blockNumber of the most recent response
|
|
56
|
-
// that updated
|
|
57
|
-
// when parallel query responses arrive out of order.
|
|
58
|
-
|
|
65
|
+
// that updated sourceRangeCapacity. Prevents degradation of the chunking
|
|
66
|
+
// heuristic when parallel query responses arrive out of order.
|
|
67
|
+
latestSourceRangeCapacityUpdateBlock: int,
|
|
59
68
|
}
|
|
60
69
|
|
|
61
70
|
type query = {
|
|
@@ -63,12 +72,15 @@ type query = {
|
|
|
63
72
|
fromBlock: int,
|
|
64
73
|
toBlock: option<int>,
|
|
65
74
|
isChunk: bool,
|
|
66
|
-
// Items this query targets: the server-side maxNumLogs-style cap,
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
// partition with no signal yet it's whatever budget share the query was
|
|
70
|
-
// sized against.
|
|
75
|
+
// Items this query targets: the server-side maxNumLogs-style cap, sized
|
|
76
|
+
// with headroom (chunkItemsMultiplier) so a denser-than-expected range
|
|
77
|
+
// doesn't truncate the response.
|
|
71
78
|
itemsTarget: int,
|
|
79
|
+
// Expected items without headroom: density × the query's block range for a
|
|
80
|
+
// known-density partition, the query's budget share otherwise. This is the
|
|
81
|
+
// unit the chain's per-tick budget is reserved/consumed in — reserving the
|
|
82
|
+
// headroomed cap instead would throttle the pipeline by the safety margin.
|
|
83
|
+
itemsEst: int,
|
|
72
84
|
selection: selection,
|
|
73
85
|
addressesByContractName: dict<array<Address.t>>,
|
|
74
86
|
}
|
|
@@ -106,22 +118,17 @@ let densityItemsTarget = (~density, ~fromBlock, ~toBlock, ~chainTargetBlock) =>
|
|
|
106
118
|
)
|
|
107
119
|
}
|
|
108
120
|
|
|
109
|
-
// Calculate the chunk range from
|
|
121
|
+
// Calculate the chunk range from the last two source-capacity observations.
|
|
110
122
|
let getMinHistoryRange = (p: partition) => {
|
|
111
|
-
switch (p.
|
|
123
|
+
switch (p.sourceRangeCapacity, p.prevSourceRangeCapacity) {
|
|
112
124
|
| (0, _) | (_, 0) => None
|
|
113
125
|
| (a, b) => Some(a < b ? a : b)
|
|
114
126
|
}
|
|
115
127
|
}
|
|
116
128
|
|
|
117
|
-
// Density
|
|
118
|
-
//
|
|
119
|
-
let getTrustedDensity = (p: partition) =>
|
|
120
|
-
switch (p.prevQueryRange, p.prevPrevQueryRange) {
|
|
121
|
-
| (0, _) | (_, 0) => None
|
|
122
|
-
| (prevQueryRange, _) => Some(p.prevRangeSize->Int.toFloat /. prevQueryRange->Int.toFloat)
|
|
123
|
-
}
|
|
124
|
-
}
|
|
129
|
+
// Density has its own initialization signal and is useful independently from
|
|
130
|
+
// source-capacity history, including when an itemsTarget cap truncates a response.
|
|
131
|
+
let getTrustedDensity = (p: partition) => p.eventDensity
|
|
125
132
|
|
|
126
133
|
let getMinQueryRange = (partitions: array<partition>) => {
|
|
127
134
|
let min = ref(0)
|
|
@@ -129,8 +136,8 @@ let getMinQueryRange = (partitions: array<partition>) => {
|
|
|
129
136
|
let p = partitions->Array.getUnsafe(i)
|
|
130
137
|
|
|
131
138
|
// Even if it's fetching, set dynamicContract field
|
|
132
|
-
let a = p.
|
|
133
|
-
let b = p.
|
|
139
|
+
let a = p.sourceRangeCapacity
|
|
140
|
+
let b = p.prevSourceRangeCapacity
|
|
134
141
|
if a > 0 && (min.contents == 0 || a < min.contents) {
|
|
135
142
|
min := a
|
|
136
143
|
}
|
|
@@ -203,11 +210,14 @@ module OptimizedPartitions = {
|
|
|
203
210
|
nextPartitionIndexRef := nextPartitionIndexRef.contents + 1
|
|
204
211
|
let minRange = getMinQueryRange([p1, p2])
|
|
205
212
|
// The merged partition indexes both parents' addresses, so its expected
|
|
206
|
-
// event rate is the sum of their densities.
|
|
207
|
-
// density
|
|
208
|
-
//
|
|
209
|
-
let inheritedDensity =
|
|
210
|
-
|
|
213
|
+
// event rate is the sum of their densities. If neither parent has a
|
|
214
|
+
// trusted density, the merged partition probes for a fresh signal
|
|
215
|
+
// instead of treating zero as an observed density.
|
|
216
|
+
let inheritedDensity = switch (p1->getTrustedDensity, p2->getTrustedDensity) {
|
|
217
|
+
| (Some(p1Density), Some(p2Density)) => Some(p1Density +. p2Density)
|
|
218
|
+
| (Some(density), None) | (None, Some(density)) => Some(density)
|
|
219
|
+
| (None, None) => None
|
|
220
|
+
}
|
|
211
221
|
{
|
|
212
222
|
id: newId,
|
|
213
223
|
dynamicContract: Some(contractName),
|
|
@@ -216,10 +226,10 @@ module OptimizedPartitions = {
|
|
|
216
226
|
mergeBlock: None,
|
|
217
227
|
addressesByContractName: Dict.make(), // set below
|
|
218
228
|
mutPendingQueries: [],
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
229
|
+
sourceRangeCapacity: minRange,
|
|
230
|
+
prevSourceRangeCapacity: minRange,
|
|
231
|
+
eventDensity: inheritedDensity,
|
|
232
|
+
latestSourceRangeCapacityUpdateBlock: 0,
|
|
223
233
|
}
|
|
224
234
|
}
|
|
225
235
|
|
|
@@ -420,14 +430,19 @@ module OptimizedPartitions = {
|
|
|
420
430
|
) => {
|
|
421
431
|
let latestFetchedBlock = ref(initialLatestFetchedBlock)
|
|
422
432
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
433
|
+
let consumedCount = ref(0)
|
|
434
|
+
let canContinue = ref(true)
|
|
435
|
+
while canContinue.contents {
|
|
436
|
+
switch mutPendingQueries->Array.get(consumedCount.contents) {
|
|
437
|
+
| Some({fetchedBlock: Some(fetchedBlock), fromBlock})
|
|
438
|
+
if fromBlock <= latestFetchedBlock.contents.blockNumber + 1 =>
|
|
439
|
+
latestFetchedBlock := fetchedBlock
|
|
440
|
+
consumedCount := consumedCount.contents + 1
|
|
441
|
+
| _ => canContinue := false
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if consumedCount.contents > 0 {
|
|
445
|
+
mutPendingQueries->Array.splice(~start=0, ~remove=consumedCount.contents, ~insert=[])->ignore
|
|
431
446
|
}
|
|
432
447
|
|
|
433
448
|
latestFetchedBlock.contents
|
|
@@ -467,28 +482,51 @@ module OptimizedPartitions = {
|
|
|
467
482
|
pendingQuery.fetchedBlock = Some(latestFetchedBlock)
|
|
468
483
|
|
|
469
484
|
let blockRange = latestFetchedBlock.blockNumber - query.fromBlock + 1
|
|
470
|
-
//
|
|
485
|
+
// Update density for every response, independently from whether this range
|
|
486
|
+
// is valid evidence of source capacity. A cap hit is still useful density
|
|
487
|
+
// evidence because it reports items returned across the scanned range.
|
|
488
|
+
let observedEventDensity = itemsCount->Int.toFloat /. blockRange->Int.toFloat
|
|
489
|
+
// Seed from the first observation, then smooth every later observation
|
|
490
|
+
// with a 1:1 moving average. Keeping initialization explicit is important:
|
|
491
|
+
// zero is valid density evidence and must participate in the next blend.
|
|
492
|
+
let updatedEventDensity = switch p.eventDensity {
|
|
493
|
+
| None => Some(observedEventDensity)
|
|
494
|
+
| Some(eventDensity) => Some((eventDensity +. observedEventDensity) /. 2.)
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Skip updating source capacity if a later response already updated it.
|
|
471
498
|
// Prevents degradation of the chunking heuristic when parallel query
|
|
472
499
|
// responses arrive out of order (e.g. earlier query with smaller range
|
|
473
500
|
// arriving after a later query with bigger range).
|
|
474
|
-
let
|
|
475
|
-
latestFetchedBlock.blockNumber > p.
|
|
501
|
+
let shouldUpdateSourceRangeCapacity =
|
|
502
|
+
latestFetchedBlock.blockNumber > p.latestSourceRangeCapacityUpdateBlock &&
|
|
476
503
|
switch query.toBlock {
|
|
477
|
-
| None =>
|
|
504
|
+
| None =>
|
|
505
|
+
// Don't update source capacity when very close to the head.
|
|
506
|
+
latestFetchedBlock.blockNumber < knownHeight - 10
|
|
478
507
|
| Some(queryToBlock) =>
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
508
|
+
if latestFetchedBlock.blockNumber < queryToBlock {
|
|
509
|
+
// Partial response is direct capacity evidence — unless it was
|
|
510
|
+
// truncated by our own itemsTarget cap: that reflects the
|
|
511
|
+
// reservation we asked for, not what the server could return.
|
|
512
|
+
itemsCount < query.itemsTarget
|
|
513
|
+
} else {
|
|
514
|
+
// A full response updates only when the query's intended range
|
|
515
|
+
// covers at least the partition's current chunk range — meaning it
|
|
516
|
+
// was a capacity-based split chunk, not a small gap-fill whose
|
|
517
|
+
// toBlock is an artificial boundary.
|
|
484
518
|
switch getMinHistoryRange(p) {
|
|
485
519
|
| None => false // Chunking not active yet, don't update
|
|
486
520
|
| Some(minHistoryRange) => queryToBlock - query.fromBlock + 1 >= minHistoryRange
|
|
487
521
|
}
|
|
522
|
+
}
|
|
488
523
|
}
|
|
489
|
-
let
|
|
490
|
-
|
|
491
|
-
|
|
524
|
+
let updatedSourceRangeCapacity = shouldUpdateSourceRangeCapacity
|
|
525
|
+
? blockRange
|
|
526
|
+
: p.sourceRangeCapacity
|
|
527
|
+
let updatedPrevSourceRangeCapacity = shouldUpdateSourceRangeCapacity
|
|
528
|
+
? p.sourceRangeCapacity
|
|
529
|
+
: p.prevSourceRangeCapacity
|
|
492
530
|
|
|
493
531
|
// Process fetched queries from front of queue for main partition
|
|
494
532
|
let updatedLatestFetchedBlock = consumeFetchedQueries(
|
|
@@ -504,28 +542,65 @@ module OptimizedPartitions = {
|
|
|
504
542
|
|
|
505
543
|
if partitionReachedMergeBlock {
|
|
506
544
|
mutEntities->Utils.Dict.deleteInPlace(p.id)
|
|
545
|
+
|
|
546
|
+
// Re-optimize to maintain sorted order and apply optimizations
|
|
547
|
+
make(
|
|
548
|
+
~partitions=mutEntities->Dict.valuesToArray,
|
|
549
|
+
~maxAddrInPartition=optimizedPartitions.maxAddrInPartition,
|
|
550
|
+
~nextPartitionIndex=optimizedPartitions.nextPartitionIndex,
|
|
551
|
+
~dynamicContracts=optimizedPartitions.dynamicContracts,
|
|
552
|
+
)
|
|
507
553
|
} else {
|
|
508
554
|
let updatedMainPartition = {
|
|
509
555
|
...p,
|
|
510
556
|
latestFetchedBlock: updatedLatestFetchedBlock,
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
557
|
+
sourceRangeCapacity: updatedSourceRangeCapacity,
|
|
558
|
+
prevSourceRangeCapacity: updatedPrevSourceRangeCapacity,
|
|
559
|
+
eventDensity: updatedEventDensity,
|
|
560
|
+
latestSourceRangeCapacityUpdateBlock: shouldUpdateSourceRangeCapacity
|
|
515
561
|
? latestFetchedBlock.blockNumber
|
|
516
|
-
: p.
|
|
562
|
+
: p.latestSourceRangeCapacityUpdateBlock,
|
|
517
563
|
}
|
|
518
564
|
|
|
519
565
|
mutEntities->Dict.set(p.id, updatedMainPartition)
|
|
520
|
-
}
|
|
521
566
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
567
|
+
if optimizedPartitions.dynamicContracts->Utils.Set.size === 0 {
|
|
568
|
+
// Fast path: merging only ever applies to dynamic-contract partitions,
|
|
569
|
+
// so with none registered a full make() would just re-sort. The updated
|
|
570
|
+
// partition's frontier only advances (consumeFetchedQueries never moves
|
|
571
|
+
// it back), so restoring idsInAscOrder is a single rightward walk of
|
|
572
|
+
// its id — and usually a no-op.
|
|
573
|
+
let ids = optimizedPartitions.idsInAscOrder
|
|
574
|
+
let count = ids->Array.length
|
|
575
|
+
let idx = ids->Array.indexOf(p.id)
|
|
576
|
+
let isAfter = jdx =>
|
|
577
|
+
jdx < count &&
|
|
578
|
+
(
|
|
579
|
+
mutEntities->Dict.getUnsafe(ids->Array.getUnsafe(jdx))
|
|
580
|
+
).latestFetchedBlock.blockNumber <
|
|
581
|
+
updatedMainPartition.latestFetchedBlock.blockNumber
|
|
582
|
+
if isAfter(idx + 1) {
|
|
583
|
+
let reordered = ids->Array.copy
|
|
584
|
+
let jdx = ref(idx)
|
|
585
|
+
while isAfter(jdx.contents + 1) {
|
|
586
|
+
reordered->Array.setUnsafe(jdx.contents, ids->Array.getUnsafe(jdx.contents + 1))
|
|
587
|
+
jdx := jdx.contents + 1
|
|
588
|
+
}
|
|
589
|
+
reordered->Array.setUnsafe(jdx.contents, p.id)
|
|
590
|
+
{...optimizedPartitions, entities: mutEntities, idsInAscOrder: reordered}
|
|
591
|
+
} else {
|
|
592
|
+
{...optimizedPartitions, entities: mutEntities}
|
|
593
|
+
}
|
|
594
|
+
} else {
|
|
595
|
+
// Re-optimize to check for merge opportunities and maintain sorted order
|
|
596
|
+
make(
|
|
597
|
+
~partitions=mutEntities->Dict.valuesToArray,
|
|
598
|
+
~maxAddrInPartition=optimizedPartitions.maxAddrInPartition,
|
|
599
|
+
~nextPartitionIndex=optimizedPartitions.nextPartitionIndex,
|
|
600
|
+
~dynamicContracts=optimizedPartitions.dynamicContracts,
|
|
601
|
+
)
|
|
602
|
+
}
|
|
603
|
+
}
|
|
529
604
|
}
|
|
530
605
|
|
|
531
606
|
@inline
|
|
@@ -618,13 +693,90 @@ let bufferReadyCount = (fetchState: t) => {
|
|
|
618
693
|
/*
|
|
619
694
|
Comparitor for two events from the same chain. No need for chain id or timestamp
|
|
620
695
|
*/
|
|
621
|
-
let
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
696
|
+
let getRegistrationIndex = (item: Internal.item): int =>
|
|
697
|
+
switch item {
|
|
698
|
+
| Event({onEventRegistration}) => onEventRegistration.index
|
|
699
|
+
| Block({onBlockRegistration}) => onBlockRegistration.index
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Total order on buffer items: block, then logIndex, then registration index.
|
|
703
|
+
// Returns a plain int (-1/0/1) with explicit field comparisons so it can be
|
|
704
|
+
// called directly from the merge/insertion loops below — no Array.sort callback,
|
|
705
|
+
// no allocated key. `0` means a true duplicate: same log routed to the same
|
|
706
|
+
// registration (two registrations for one log differ by index and are kept).
|
|
707
|
+
let compareBufferItem = (a: Internal.item, b: Internal.item): int => {
|
|
708
|
+
let ba = a->Internal.getItemBlockNumber
|
|
709
|
+
let bb = b->Internal.getItemBlockNumber
|
|
710
|
+
if ba != bb {
|
|
711
|
+
ba < bb ? -1 : 1
|
|
625
712
|
} else {
|
|
626
|
-
|
|
713
|
+
let la = a->Internal.getItemLogIndex
|
|
714
|
+
let lb = b->Internal.getItemLogIndex
|
|
715
|
+
if la != lb {
|
|
716
|
+
la < lb ? -1 : 1
|
|
717
|
+
} else {
|
|
718
|
+
let ia = a->getRegistrationIndex
|
|
719
|
+
let ib = b->getRegistrationIndex
|
|
720
|
+
ia < ib ? -1 : ia > ib ? 1 : 0
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Merge a maybe-unsorted `newItems` run into the already-sorted, already-deduped
|
|
726
|
+
// `buffer`, dropping items equal on (blockNumber, logIndex, registration index).
|
|
727
|
+
// Single linear pass over both runs after ordering `newItems` in place; every
|
|
728
|
+
// comparison is a direct `compareBufferItem` call (V8 inlines it) rather than a
|
|
729
|
+
// callback through `Array.sort`.
|
|
730
|
+
let mergeIntoBuffer = (buffer: array<Internal.item>, newItems: array<Internal.item>): array<
|
|
731
|
+
Internal.item,
|
|
732
|
+
> => {
|
|
733
|
+
let n = newItems->Array.length
|
|
734
|
+
// Insertion sort: a source response is small and usually already ascending,
|
|
735
|
+
// so this is ~O(n) here.
|
|
736
|
+
for i in 1 to n - 1 {
|
|
737
|
+
let x = newItems->Array.getUnsafe(i)
|
|
738
|
+
let j = ref(i - 1)
|
|
739
|
+
while j.contents >= 0 && compareBufferItem(newItems->Array.getUnsafe(j.contents), x) > 0 {
|
|
740
|
+
newItems->Array.setUnsafe(j.contents + 1, newItems->Array.getUnsafe(j.contents))
|
|
741
|
+
j := j.contents - 1
|
|
742
|
+
}
|
|
743
|
+
newItems->Array.setUnsafe(j.contents + 1, x)
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
let m = buffer->Array.length
|
|
747
|
+
let merged = []
|
|
748
|
+
let last = ref(None)
|
|
749
|
+
let push = item =>
|
|
750
|
+
switch last.contents {
|
|
751
|
+
| Some(l) if compareBufferItem(l, item) === 0 => ()
|
|
752
|
+
| _ => {
|
|
753
|
+
merged->Array.push(item)
|
|
754
|
+
last := Some(item)
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
let i = ref(0)
|
|
759
|
+
let j = ref(0)
|
|
760
|
+
while i.contents < m && j.contents < n {
|
|
761
|
+
let a = buffer->Array.getUnsafe(i.contents)
|
|
762
|
+
let b = newItems->Array.getUnsafe(j.contents)
|
|
763
|
+
if compareBufferItem(a, b) <= 0 {
|
|
764
|
+
push(a)
|
|
765
|
+
i := i.contents + 1
|
|
766
|
+
} else {
|
|
767
|
+
push(b)
|
|
768
|
+
j := j.contents + 1
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
while i.contents < m {
|
|
772
|
+
push(buffer->Array.getUnsafe(i.contents))
|
|
773
|
+
i := i.contents + 1
|
|
627
774
|
}
|
|
775
|
+
while j.contents < n {
|
|
776
|
+
push(newItems->Array.getUnsafe(j.contents))
|
|
777
|
+
j := j.contents + 1
|
|
778
|
+
}
|
|
779
|
+
merged
|
|
628
780
|
}
|
|
629
781
|
|
|
630
782
|
// Some big number which should be bigger than any log index
|
|
@@ -695,48 +847,46 @@ let updateInternal = (
|
|
|
695
847
|
fetchState: t,
|
|
696
848
|
~optimizedPartitions=fetchState.optimizedPartitions,
|
|
697
849
|
~mutItems=?,
|
|
850
|
+
// Set when the caller already passes a sorted, deduped buffer (hot paths merge
|
|
851
|
+
// via mergeIntoBuffer or filter the sorted buffer). Otherwise mutItems is
|
|
852
|
+
// normalized here, so callers can hand over items in any order.
|
|
853
|
+
~mutItemsSorted=false,
|
|
698
854
|
~blockLag=fetchState.blockLag,
|
|
699
855
|
~knownHeight=fetchState.knownHeight,
|
|
700
856
|
): t => {
|
|
701
|
-
|
|
857
|
+
// The buffer to build on: the caller's items (normalized to sorted if needed),
|
|
858
|
+
// or the current buffer when only onBlock items change.
|
|
859
|
+
let base = switch mutItems {
|
|
860
|
+
| Some(items) => mutItemsSorted ? items : []->mergeIntoBuffer(items)
|
|
861
|
+
| None => fetchState.buffer
|
|
862
|
+
}
|
|
702
863
|
|
|
864
|
+
// onBlock items are generated as their own ascending (block, logIndex) run and
|
|
865
|
+
// folded into `base` by the single merge below.
|
|
866
|
+
let blockItems = []
|
|
703
867
|
let latestOnBlockBlockNumber = switch fetchState.onBlockRegistrations {
|
|
704
868
|
| [] => knownHeight
|
|
705
|
-
| onBlockRegistrations =>
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
|
715
|
-
|
|
|
716
|
-
}->Array.get(fetchState.maxOnBlockBufferSize - 1) {
|
|
717
|
-
| Some(item) => item->Internal.getItemBlockNumber
|
|
718
|
-
| None =>
|
|
719
|
-
switch optimizedPartitions->OptimizedPartitions.getLatestFullyFetchedBlock {
|
|
720
|
-
| None => knownHeight
|
|
721
|
-
| Some(latestFullyFetchedBlock) => latestFullyFetchedBlock.blockNumber
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
let mutItems = switch mutItemsRef.contents {
|
|
726
|
-
| Some(mutItems) => mutItems
|
|
727
|
-
| None => fetchState.buffer->Array.copy
|
|
869
|
+
| onBlockRegistrations =>
|
|
870
|
+
// Calculate the max block number we are going to create items for
|
|
871
|
+
// Use maxOnBlockBufferSize to get the last target item in the buffer
|
|
872
|
+
// (sorted, so this is the highest-block item within the buffer cap).
|
|
873
|
+
// All this needed to prevent OOM when adding too many block items to the queue
|
|
874
|
+
let maxBlockNumber = switch base->Array.get(fetchState.maxOnBlockBufferSize - 1) {
|
|
875
|
+
| Some(item) => item->Internal.getItemBlockNumber
|
|
876
|
+
| None =>
|
|
877
|
+
switch optimizedPartitions->OptimizedPartitions.getLatestFullyFetchedBlock {
|
|
878
|
+
| None => knownHeight
|
|
879
|
+
| Some(latestFullyFetchedBlock) => latestFullyFetchedBlock.blockNumber
|
|
728
880
|
}
|
|
729
|
-
mutItemsRef := Some(mutItems)
|
|
730
|
-
|
|
731
|
-
appendOnBlockItems(
|
|
732
|
-
~mutItems,
|
|
733
|
-
~onBlockRegistrations,
|
|
734
|
-
~indexerStartBlock=fetchState.startBlock,
|
|
735
|
-
~fromBlock=fetchState.latestOnBlockBlockNumber,
|
|
736
|
-
~maxBlockNumber,
|
|
737
|
-
~maxOnBlockBufferSize=fetchState.maxOnBlockBufferSize,
|
|
738
|
-
)
|
|
739
881
|
}
|
|
882
|
+
appendOnBlockItems(
|
|
883
|
+
~mutItems=blockItems,
|
|
884
|
+
~onBlockRegistrations,
|
|
885
|
+
~indexerStartBlock=fetchState.startBlock,
|
|
886
|
+
~fromBlock=fetchState.latestOnBlockBlockNumber,
|
|
887
|
+
~maxBlockNumber,
|
|
888
|
+
~maxOnBlockBufferSize=fetchState.maxOnBlockBufferSize,
|
|
889
|
+
)
|
|
740
890
|
}
|
|
741
891
|
|
|
742
892
|
let updatedFetchState = {
|
|
@@ -751,15 +901,10 @@ let updateInternal = (
|
|
|
751
901
|
latestOnBlockBlockNumber,
|
|
752
902
|
blockLag,
|
|
753
903
|
knownHeight,
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
| Some(mutItems) => {
|
|
759
|
-
mutItems->Array.sort(compareBufferItem)
|
|
760
|
-
mutItems
|
|
761
|
-
}
|
|
762
|
-
| None => fetchState.buffer
|
|
904
|
+
// Single merge point: fold any onBlock items into the sorted base buffer.
|
|
905
|
+
buffer: switch blockItems {
|
|
906
|
+
| [] => base
|
|
907
|
+
| blockItems => base->mergeIntoBuffer(blockItems)
|
|
763
908
|
},
|
|
764
909
|
firstEventBlock: fetchState.firstEventBlock,
|
|
765
910
|
}
|
|
@@ -905,10 +1050,10 @@ OptimizedPartitions.t => {
|
|
|
905
1050
|
addressesByContractName,
|
|
906
1051
|
mergeBlock: None,
|
|
907
1052
|
mutPendingQueries: [],
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
1053
|
+
sourceRangeCapacity: 0,
|
|
1054
|
+
prevSourceRangeCapacity: 0,
|
|
1055
|
+
eventDensity: None,
|
|
1056
|
+
latestSourceRangeCapacityUpdateBlock: 0,
|
|
912
1057
|
})
|
|
913
1058
|
nextPartitionIndexRef := nextPartitionIndexRef.contents + 1
|
|
914
1059
|
}
|
|
@@ -1241,10 +1386,10 @@ let registerDynamicContracts = (
|
|
|
1241
1386
|
addressesByContractName,
|
|
1242
1387
|
mergeBlock: None,
|
|
1243
1388
|
mutPendingQueries: p.mutPendingQueries,
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1389
|
+
sourceRangeCapacity: p.sourceRangeCapacity,
|
|
1390
|
+
prevSourceRangeCapacity: p.prevSourceRangeCapacity,
|
|
1391
|
+
eventDensity: p.eventDensity,
|
|
1392
|
+
latestSourceRangeCapacityUpdateBlock: p.latestSourceRangeCapacityUpdateBlock,
|
|
1248
1393
|
})
|
|
1249
1394
|
}
|
|
1250
1395
|
}
|
|
@@ -1276,6 +1421,26 @@ let registerDynamicContracts = (
|
|
|
1276
1421
|
}
|
|
1277
1422
|
}
|
|
1278
1423
|
|
|
1424
|
+
// Drop events an address-param filter rejects. A merged partition may
|
|
1425
|
+
// over-fetch a wildcard event whose indexed address param references an
|
|
1426
|
+
// address registered after the log's block; `clientAddressFilter` is the
|
|
1427
|
+
// param-level analogue of EventRouter's srcAddress effectiveStartBlock check.
|
|
1428
|
+
let filterByClientAddress = (
|
|
1429
|
+
items: array<Internal.item>,
|
|
1430
|
+
~indexingAddresses: IndexingAddresses.t,
|
|
1431
|
+
): array<Internal.item> =>
|
|
1432
|
+
items->Array.filter(item =>
|
|
1433
|
+
switch item {
|
|
1434
|
+
| Internal.Event({payload, blockNumber}) as item =>
|
|
1435
|
+
switch (item->Internal.castUnsafeEventItem).onEventRegistration.clientAddressFilter {
|
|
1436
|
+
| Some(filter) =>
|
|
1437
|
+
filter(payload, blockNumber, indexingAddresses->IndexingAddresses.rawForFilter)
|
|
1438
|
+
| None => true
|
|
1439
|
+
}
|
|
1440
|
+
| _ => true
|
|
1441
|
+
}
|
|
1442
|
+
)
|
|
1443
|
+
|
|
1279
1444
|
/*
|
|
1280
1445
|
Updates fetchState with a response for a given query.
|
|
1281
1446
|
Returns Error if the partition with given query cannot be found (unexpected)
|
|
@@ -1284,26 +1449,10 @@ newItems are ordered earliest to latest (as they are returned from the worker)
|
|
|
1284
1449
|
*/
|
|
1285
1450
|
let handleQueryResult = (
|
|
1286
1451
|
fetchState: t,
|
|
1287
|
-
~indexingAddresses: IndexingAddresses.t,
|
|
1288
1452
|
~query: query,
|
|
1289
1453
|
~latestFetchedBlock: blockNumberAndTimestamp,
|
|
1290
1454
|
~newItems,
|
|
1291
1455
|
): t => {
|
|
1292
|
-
// Drop events an address-param filter rejects. A merged partition may
|
|
1293
|
-
// over-fetch a wildcard event whose indexed address param references an
|
|
1294
|
-
// address registered after the log's block; `clientAddressFilter` is the
|
|
1295
|
-
// param-level analogue of EventRouter's srcAddress effectiveStartBlock check.
|
|
1296
|
-
let newItems = newItems->Array.filter(item =>
|
|
1297
|
-
switch item {
|
|
1298
|
-
| Internal.Event({onEventRegistration, payload, blockNumber}) =>
|
|
1299
|
-
switch onEventRegistration.clientAddressFilter {
|
|
1300
|
-
| Some(filter) =>
|
|
1301
|
-
filter(payload, blockNumber, indexingAddresses->IndexingAddresses.rawForFilter)
|
|
1302
|
-
| None => true
|
|
1303
|
-
}
|
|
1304
|
-
| _ => true
|
|
1305
|
-
}
|
|
1306
|
-
)
|
|
1307
1456
|
fetchState->updateInternal(
|
|
1308
1457
|
~optimizedPartitions=fetchState.optimizedPartitions->OptimizedPartitions.handleQueryResponse(
|
|
1309
1458
|
~query,
|
|
@@ -1311,10 +1460,14 @@ let handleQueryResult = (
|
|
|
1311
1460
|
~itemsCount=newItems->Array.length,
|
|
1312
1461
|
~latestFetchedBlock,
|
|
1313
1462
|
),
|
|
1463
|
+
// Merge the response into the sorted buffer, dropping duplicates an
|
|
1464
|
+
// overlapping query may re-deliver (e.g. an over-fetched log matched by two
|
|
1465
|
+
// partitions). Absorbs sorting too, so updateInternal doesn't re-sort.
|
|
1466
|
+
~mutItemsSorted=true,
|
|
1314
1467
|
~mutItems=?{
|
|
1315
1468
|
switch newItems {
|
|
1316
1469
|
| [] => None
|
|
1317
|
-
| _ => Some(fetchState.buffer->
|
|
1470
|
+
| _ => Some(fetchState.buffer->mergeIntoBuffer(newItems))
|
|
1318
1471
|
}
|
|
1319
1472
|
},
|
|
1320
1473
|
)
|
|
@@ -1335,6 +1488,7 @@ let startFetchingQueries = ({optimizedPartitions}: t, ~queries: array<query>) =>
|
|
|
1335
1488
|
toBlock: q.toBlock,
|
|
1336
1489
|
isChunk: q.isChunk,
|
|
1337
1490
|
itemsTarget: q.itemsTarget,
|
|
1491
|
+
itemsEst: q.itemsEst,
|
|
1338
1492
|
fetchedBlock: None,
|
|
1339
1493
|
}
|
|
1340
1494
|
|
|
@@ -1356,61 +1510,129 @@ let startFetchingQueries = ({optimizedPartitions}: t, ~queries: array<query>) =>
|
|
|
1356
1510
|
}
|
|
1357
1511
|
|
|
1358
1512
|
// Most parallel in-flight chunk queries a single partition may have at once.
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
//
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
//
|
|
1365
|
-
//
|
|
1366
|
-
//
|
|
1367
|
-
|
|
1368
|
-
|
|
1513
|
+
// Only queries still being fetched count — a fetched chunk parked behind a gap
|
|
1514
|
+
// doesn't hold a slot, so a slow query at the queue head can't starve the
|
|
1515
|
+
// partition's pipeline.
|
|
1516
|
+
let maxInFlightChunksPerPartition = 12
|
|
1517
|
+
|
|
1518
|
+
// Most parallel in-flight queries a single chain may have at once, across all
|
|
1519
|
+
// its partitions. Bounds source load on chains with many partitions, where the
|
|
1520
|
+
// per-partition cap alone would admit thousands of concurrent queries.
|
|
1521
|
+
let maxChainConcurrency = 100
|
|
1522
|
+
|
|
1523
|
+
// Chunk spans grow by this factor over the smallest recently observed source
|
|
1524
|
+
// range, so the pipeline keeps probing for more capacity instead of locking in
|
|
1525
|
+
// the first measurement.
|
|
1526
|
+
let chunkRangeGrowthFactor = 1.8
|
|
1527
|
+
|
|
1528
|
+
// Push one density-priced query and return its itemsEst. itemsEst is the honest
|
|
1529
|
+
// density estimate the chain's budget is reserved in; itemsTarget is the
|
|
1530
|
+
// server-side cap with chunkItemsMultiplier headroom so a denser-than-expected
|
|
1531
|
+
// range doesn't truncate the response.
|
|
1532
|
+
//
|
|
1533
|
+
// A bounded query (toBlock set) additionally floors its cap at
|
|
1534
|
+
// itemsTargetFloor: its range is already the hard bound on the response, so a
|
|
1535
|
+
// low density estimate shrinking the cap only buys self-truncated responses —
|
|
1536
|
+
// each one a wasted roundtrip that opens a gap and pollutes nothing but our
|
|
1537
|
+
// own pipeline. The floor is the indexer target split across the chain's
|
|
1538
|
+
// concurrency slots, so even every in-flight bounded query hitting its floored
|
|
1539
|
+
// cap at once overshoots the pool by at most ~one buffer target. Open-ended
|
|
1540
|
+
// queries keep the pure density cap — there it's the only bound at all.
|
|
1541
|
+
let pushDensityPricedQuery = (
|
|
1542
|
+
queries: array<query>,
|
|
1543
|
+
~partitionId,
|
|
1544
|
+
~fromBlock,
|
|
1545
|
+
~toBlock,
|
|
1546
|
+
~isChunk,
|
|
1547
|
+
~density,
|
|
1548
|
+
~chunkItemsMultiplier,
|
|
1549
|
+
~chainTargetBlock,
|
|
1550
|
+
~itemsTargetFloor,
|
|
1551
|
+
~selection,
|
|
1552
|
+
~addressesByContractName,
|
|
1553
|
+
) => {
|
|
1554
|
+
let itemsEst = densityItemsTarget(~density, ~fromBlock, ~toBlock, ~chainTargetBlock)
|
|
1555
|
+
let itemsTarget = densityItemsTarget(
|
|
1556
|
+
~density=density *. chunkItemsMultiplier,
|
|
1557
|
+
~fromBlock,
|
|
1558
|
+
~toBlock,
|
|
1559
|
+
~chainTargetBlock,
|
|
1560
|
+
)
|
|
1561
|
+
queries
|
|
1562
|
+
->Array.push({
|
|
1563
|
+
partitionId,
|
|
1564
|
+
fromBlock,
|
|
1565
|
+
toBlock,
|
|
1566
|
+
selection,
|
|
1567
|
+
isChunk,
|
|
1568
|
+
itemsTarget: switch toBlock {
|
|
1569
|
+
| Some(_) => Pervasives.max(itemsTarget, itemsTargetFloor)
|
|
1570
|
+
| None => itemsTarget
|
|
1571
|
+
},
|
|
1572
|
+
itemsEst,
|
|
1573
|
+
addressesByContractName,
|
|
1574
|
+
})
|
|
1575
|
+
->ignore
|
|
1576
|
+
itemsEst
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// Generates candidate queries for a gap range (a hole left between
|
|
1580
|
+
// completed/pending chunks, e.g. from an out-of-order partial response). Gaps
|
|
1581
|
+
// carry the range's low fromBlock, so the acceptance pass takes them before
|
|
1582
|
+
// forward progress. Chunks only on a trusted POSITIVE density; a positive
|
|
1583
|
+
// density without capacity history prices a single query. Zero or unknown
|
|
1584
|
+
// density falls back to the "available density" — the partition's equal-divide
|
|
1585
|
+
// budget spread over its remaining range this tick. That fallback should be
|
|
1586
|
+
// unreachable (a gap implies pipelined chunks, which imply a positive observed
|
|
1587
|
+
// density), but if chunking preconditions ever change it must not price by a
|
|
1588
|
+
// zero density: that would produce an itemsTarget-1 query crawling a dense gap
|
|
1589
|
+
// one item per response.
|
|
1369
1590
|
let pushGapFillQueries = (
|
|
1370
1591
|
queries: array<query>,
|
|
1371
1592
|
~partitionId: string,
|
|
1372
1593
|
~rangeFromBlock: int,
|
|
1373
1594
|
~rangeEndBlock: option<int>,
|
|
1374
|
-
~
|
|
1595
|
+
~headBlockNumber: int,
|
|
1375
1596
|
~chainTargetBlock: int,
|
|
1376
1597
|
~maybeChunkRange: option<int>,
|
|
1377
1598
|
~maxChunks: int,
|
|
1378
1599
|
~partition: partition,
|
|
1379
1600
|
~partitionBudget: float,
|
|
1601
|
+
~chunkItemsMultiplier: float,
|
|
1602
|
+
~itemsTargetFloor: int,
|
|
1380
1603
|
~selection: selection,
|
|
1381
1604
|
~addressesByContractName: dict<array<Address.t>>,
|
|
1382
1605
|
) => {
|
|
1383
|
-
|
|
1384
|
-
|
|
1606
|
+
// Gaps past the chain's target block wait: they regenerate from the
|
|
1607
|
+
// pending-walk each tick and fill once the target reaches them. The lagged
|
|
1608
|
+
// head is the fetchable ceiling — blocks past knownHeight - blockLag can't
|
|
1609
|
+
// be queried yet.
|
|
1610
|
+
if rangeFromBlock <= Pervasives.min(headBlockNumber, chainTargetBlock) && maxChunks > 0 {
|
|
1385
1611
|
switch rangeEndBlock {
|
|
1386
1612
|
| Some(endBlock) if rangeFromBlock > endBlock => ()
|
|
1387
1613
|
| _ =>
|
|
1388
|
-
let trustedDensity = partition->getTrustedDensity
|
|
1389
1614
|
let maxBlock = switch rangeEndBlock {
|
|
1390
1615
|
| Some(eb) => eb
|
|
1391
1616
|
| None => chainTargetBlock
|
|
1392
1617
|
}
|
|
1393
|
-
let pushSingleQuery = (~density, ~isChunk) =>
|
|
1394
|
-
|
|
1395
|
-
|
|
1618
|
+
let pushSingleQuery = (~density, ~isChunk) =>
|
|
1619
|
+
queries
|
|
1620
|
+
->pushDensityPricedQuery(
|
|
1621
|
+
~partitionId,
|
|
1396
1622
|
~fromBlock=rangeFromBlock,
|
|
1397
1623
|
~toBlock=rangeEndBlock,
|
|
1624
|
+
~isChunk,
|
|
1625
|
+
~density,
|
|
1626
|
+
~chunkItemsMultiplier,
|
|
1398
1627
|
~chainTargetBlock,
|
|
1628
|
+
~itemsTargetFloor,
|
|
1629
|
+
~selection,
|
|
1630
|
+
~addressesByContractName,
|
|
1399
1631
|
)
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
fromBlock: rangeFromBlock,
|
|
1403
|
-
toBlock: rangeEndBlock,
|
|
1404
|
-
selection,
|
|
1405
|
-
isChunk,
|
|
1406
|
-
itemsTarget,
|
|
1407
|
-
addressesByContractName,
|
|
1408
|
-
})
|
|
1409
|
-
cost := cost.contents +. itemsTarget->Int.toFloat
|
|
1410
|
-
}
|
|
1411
|
-
switch (trustedDensity, maybeChunkRange) {
|
|
1632
|
+
->ignore
|
|
1633
|
+
switch (partition->getTrustedDensity, maybeChunkRange) {
|
|
1412
1634
|
| (Some(density), Some(chunkRange)) if density > 0. =>
|
|
1413
|
-
let chunkSize = Js.Math.ceil_int(chunkRange->Int.toFloat *.
|
|
1635
|
+
let chunkSize = Js.Math.ceil_int(chunkRange->Int.toFloat *. chunkRangeGrowthFactor)
|
|
1414
1636
|
if rangeFromBlock + chunkSize * 2 - 1 <= maxBlock {
|
|
1415
1637
|
let chunkFromBlock = ref(rangeFromBlock)
|
|
1416
1638
|
let chunkIdx = ref(0)
|
|
@@ -1418,22 +1640,20 @@ let pushGapFillQueries = (
|
|
|
1418
1640
|
chunkIdx.contents < maxChunks && chunkFromBlock.contents + chunkSize - 1 <= maxBlock
|
|
1419
1641
|
) {
|
|
1420
1642
|
let chunkToBlock = chunkFromBlock.contents + chunkSize - 1
|
|
1421
|
-
|
|
1422
|
-
|
|
1643
|
+
queries
|
|
1644
|
+
->pushDensityPricedQuery(
|
|
1645
|
+
~partitionId,
|
|
1423
1646
|
~fromBlock=chunkFromBlock.contents,
|
|
1424
1647
|
~toBlock=Some(chunkToBlock),
|
|
1648
|
+
~isChunk=true,
|
|
1649
|
+
~density,
|
|
1650
|
+
~chunkItemsMultiplier,
|
|
1425
1651
|
~chainTargetBlock,
|
|
1652
|
+
~itemsTargetFloor,
|
|
1653
|
+
~selection,
|
|
1654
|
+
~addressesByContractName,
|
|
1426
1655
|
)
|
|
1427
|
-
|
|
1428
|
-
partitionId,
|
|
1429
|
-
fromBlock: chunkFromBlock.contents,
|
|
1430
|
-
toBlock: Some(chunkToBlock),
|
|
1431
|
-
isChunk: true,
|
|
1432
|
-
selection,
|
|
1433
|
-
itemsTarget,
|
|
1434
|
-
addressesByContractName,
|
|
1435
|
-
})
|
|
1436
|
-
cost := cost.contents +. itemsTarget->Int.toFloat
|
|
1656
|
+
->ignore
|
|
1437
1657
|
chunkFromBlock := chunkToBlock + 1
|
|
1438
1658
|
chunkIdx := chunkIdx.contents + 1
|
|
1439
1659
|
}
|
|
@@ -1441,56 +1661,283 @@ let pushGapFillQueries = (
|
|
|
1441
1661
|
// Not enough room for 2 chunks, fall back to a single query
|
|
1442
1662
|
pushSingleQuery(~density, ~isChunk=rangeEndBlock !== None)
|
|
1443
1663
|
}
|
|
1444
|
-
| (Some(density),
|
|
1445
|
-
|
|
|
1664
|
+
| (Some(density), None) if density > 0. => pushSingleQuery(~density, ~isChunk=false)
|
|
1665
|
+
| _ =>
|
|
1446
1666
|
let remainingRange = Pervasives.max(1, chainTargetBlock - rangeFromBlock + 1)
|
|
1447
1667
|
pushSingleQuery(~density=partitionBudget /. remainingRange->Int.toFloat, ~isChunk=false)
|
|
1448
1668
|
}
|
|
1449
1669
|
}
|
|
1450
1670
|
}
|
|
1451
|
-
cost.contents
|
|
1452
|
-
}
|
|
1453
|
-
|
|
1454
|
-
// The level every partition ends at when `budget` is poured across partitions
|
|
1455
|
-
// already holding `footprints`: the unique L with Σ max(0, L - fᵢ) = budget.
|
|
1456
|
-
// Partitions above L get nothing (their head start is their share); the rest
|
|
1457
|
-
// are topped up exactly to L, so the pour equals the budget no matter how
|
|
1458
|
-
// uneven the footprints are.
|
|
1459
|
-
let waterLevel = (~budget: float, ~footprints: array<float>) => {
|
|
1460
|
-
let sorted = footprints->Array.toSorted(Float.compare)
|
|
1461
|
-
let n = sorted->Array.length
|
|
1462
|
-
let prefix = ref(0.)
|
|
1463
|
-
let level = ref(None)
|
|
1464
|
-
let idx = ref(0)
|
|
1465
|
-
while level.contents == None && idx.contents < n {
|
|
1466
|
-
let i = idx.contents
|
|
1467
|
-
prefix := prefix.contents +. sorted->Array.getUnsafe(i)
|
|
1468
|
-
// The level if only the i+1 lowest footprints receive water — correct once
|
|
1469
|
-
// it doesn't reach the next footprint up.
|
|
1470
|
-
let candidate = (budget +. prefix.contents) /. (i + 1)->Int.toFloat
|
|
1471
|
-
if i == n - 1 || candidate <= sorted->Array.getUnsafe(i + 1) {
|
|
1472
|
-
level := Some(candidate)
|
|
1473
|
-
}
|
|
1474
|
-
idx := idx.contents + 1
|
|
1475
|
-
}
|
|
1476
|
-
level.contents->Option.getOr(0.)
|
|
1477
1671
|
}
|
|
1478
1672
|
|
|
1479
|
-
// Per-partition
|
|
1480
|
-
|
|
1673
|
+
// Per-partition state carried from the gap-fill/cursor walk to candidate
|
|
1674
|
+
// generation.
|
|
1675
|
+
type partitionFillState = {
|
|
1481
1676
|
partitionId: string,
|
|
1482
1677
|
p: partition,
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
//
|
|
1486
|
-
|
|
1487
|
-
|
|
1678
|
+
cursor: int,
|
|
1679
|
+
// Chunks already generated for this partition during gap-fill — used with
|
|
1680
|
+
// inFlightCount against maxInFlightChunksPerPartition.
|
|
1681
|
+
chunksUsedThisCall: int,
|
|
1682
|
+
// Still-being-fetched pending queries before this call — fixed for the call.
|
|
1683
|
+
// A fetched chunk parked behind a gap doesn't hold a pipeline slot.
|
|
1684
|
+
inFlightCount: int,
|
|
1488
1685
|
queryEndBlock: option<int>,
|
|
1489
1686
|
maybeChunkRange: option<int>,
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
// Gap-fill: walk one partition's pending queries, generating a candidate for
|
|
1690
|
+
// any hole (e.g. from an out-of-order partial chunk response). Returns the
|
|
1691
|
+
// partition's post-gap fill state — its cursor for forward work — or None when
|
|
1692
|
+
// the partition is blocked on an unresolved single-shot query.
|
|
1693
|
+
let walkPartitionPending = (
|
|
1694
|
+
p: partition,
|
|
1695
|
+
~partitionId: string,
|
|
1696
|
+
~inFlightCount: int,
|
|
1697
|
+
~candidates: array<query>,
|
|
1698
|
+
~headBlockNumber: int,
|
|
1699
|
+
~chainTargetBlock: int,
|
|
1700
|
+
~chunkItemsMultiplier: float,
|
|
1701
|
+
~itemsTargetFloor: int,
|
|
1702
|
+
~partitionBudget: float,
|
|
1703
|
+
~queryEndBlock: option<int>,
|
|
1704
|
+
): option<partitionFillState> => {
|
|
1705
|
+
let maybeChunkRange = getMinHistoryRange(p)
|
|
1706
|
+
let pendingCount = p.mutPendingQueries->Array.length
|
|
1707
|
+
|
|
1708
|
+
let cursor = ref(p.latestFetchedBlock.blockNumber + 1)
|
|
1709
|
+
let canContinue = ref(true)
|
|
1710
|
+
let chunksUsedThisCall = ref(0)
|
|
1711
|
+
let pqIdx = ref(0)
|
|
1712
|
+
while pqIdx.contents < pendingCount && canContinue.contents {
|
|
1713
|
+
let pq = p.mutPendingQueries->Array.getUnsafe(pqIdx.contents)
|
|
1714
|
+
|
|
1715
|
+
if pq.fromBlock > cursor.contents {
|
|
1716
|
+
let beforeLen = candidates->Array.length
|
|
1717
|
+
pushGapFillQueries(
|
|
1718
|
+
candidates,
|
|
1719
|
+
~partitionId,
|
|
1720
|
+
~rangeFromBlock=cursor.contents,
|
|
1721
|
+
~rangeEndBlock=Utils.Math.minOptInt(Some(pq.fromBlock - 1), queryEndBlock),
|
|
1722
|
+
~headBlockNumber,
|
|
1723
|
+
~chainTargetBlock,
|
|
1724
|
+
~maybeChunkRange,
|
|
1725
|
+
~maxChunks=maxInFlightChunksPerPartition - inFlightCount - chunksUsedThisCall.contents,
|
|
1726
|
+
~partition=p,
|
|
1727
|
+
~partitionBudget,
|
|
1728
|
+
~chunkItemsMultiplier,
|
|
1729
|
+
~itemsTargetFloor,
|
|
1730
|
+
~selection=p.selection,
|
|
1731
|
+
~addressesByContractName=p.addressesByContractName,
|
|
1732
|
+
)
|
|
1733
|
+
chunksUsedThisCall := chunksUsedThisCall.contents + (candidates->Array.length - beforeLen)
|
|
1734
|
+
}
|
|
1735
|
+
switch pq {
|
|
1736
|
+
| {isChunk: true, toBlock: Some(toBlock), fetchedBlock: Some({blockNumber})}
|
|
1737
|
+
if blockNumber < toBlock =>
|
|
1738
|
+
cursor := blockNumber + 1
|
|
1739
|
+
| {isChunk: true, toBlock: Some(toBlock)} => cursor := toBlock + 1
|
|
1740
|
+
| _ => canContinue := false
|
|
1741
|
+
}
|
|
1742
|
+
pqIdx := pqIdx.contents + 1
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
canContinue.contents
|
|
1746
|
+
? Some({
|
|
1747
|
+
partitionId,
|
|
1748
|
+
p,
|
|
1749
|
+
cursor: cursor.contents,
|
|
1750
|
+
chunksUsedThisCall: chunksUsedThisCall.contents,
|
|
1751
|
+
inFlightCount,
|
|
1752
|
+
queryEndBlock,
|
|
1753
|
+
maybeChunkRange,
|
|
1754
|
+
})
|
|
1755
|
+
: None
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
// Forward work: generate each in-range partition's candidates — strict chunks
|
|
1759
|
+
// when both source-capacity history and density are known, or an open-ended
|
|
1760
|
+
// budget probe otherwise. No budget check here; the acceptance pass decides
|
|
1761
|
+
// which candidates make the cut.
|
|
1762
|
+
//
|
|
1763
|
+
// Chunks require a POSITIVE trusted density: density 0 prices every chunk at
|
|
1764
|
+
// ~nothing, so an open-ended probe (full server scan range in one response)
|
|
1765
|
+
// beats a pipeline of hard-bounded chunks that crawl chunkRangeGrowthFactor×
|
|
1766
|
+
// per two responses.
|
|
1767
|
+
let pushForwardCandidates = (
|
|
1768
|
+
candidates: array<query>,
|
|
1769
|
+
// May be truncated to the chain's free concurrency slots — a pure generation
|
|
1770
|
+
// bound, see getNextQuery.
|
|
1771
|
+
~inRangeStates: array<partitionFillState>,
|
|
1772
|
+
// The full in-range partition count, pre-truncation. Probe sizing divides by
|
|
1773
|
+
// this so each probe's itemsEst/itemsTarget stays the honest per-partition
|
|
1774
|
+
// share for budget control — sizing by the (fewer) admittable queries would
|
|
1775
|
+
// let every accepted probe over-fetch its share.
|
|
1776
|
+
~inRangeCount: int,
|
|
1777
|
+
~chainTargetBlock: int,
|
|
1778
|
+
~freshBudget: float,
|
|
1779
|
+
~chunkItemsMultiplier: float,
|
|
1780
|
+
~itemsTargetFloor: int,
|
|
1781
|
+
) => {
|
|
1782
|
+
// Even share of the fresh budget across the partitions actually fetching
|
|
1783
|
+
// this tick (not every partition — so budget isn't stranded on ones below
|
|
1784
|
+
// the head, waiting, or already done). The fallback when there's no range to
|
|
1785
|
+
// the target.
|
|
1786
|
+
let probeShare = inRangeCount == 0 ? 0. : freshBudget /. inRangeCount->Int.toFloat
|
|
1787
|
+
// Items/block the budget implies over the range those partitions cover this
|
|
1788
|
+
// tick — from the furthest-behind in-range cursor to the target. A probe
|
|
1789
|
+
// covering less of that range (its partition sits further ahead) gets
|
|
1790
|
+
// proportionally fewer items; one starting at the frontier gets the full
|
|
1791
|
+
// even share.
|
|
1792
|
+
let frontierCursor =
|
|
1793
|
+
inRangeStates->Array.reduce(chainTargetBlock, (min, fs) => fs.cursor < min ? fs.cursor : min)
|
|
1794
|
+
let rangeToTarget = chainTargetBlock - frontierCursor + 1
|
|
1795
|
+
let rangeTargetDensity =
|
|
1796
|
+
inRangeCount > 0 && rangeToTarget > 0 ? freshBudget /. rangeToTarget->Int.toFloat : 0.
|
|
1797
|
+
|
|
1798
|
+
inRangeStates->Array.forEach(fs => {
|
|
1799
|
+
let p = fs.p
|
|
1800
|
+
let maxBlock = switch fs.queryEndBlock {
|
|
1801
|
+
| Some(eb) => eb
|
|
1802
|
+
| None => chainTargetBlock
|
|
1803
|
+
}
|
|
1804
|
+
switch (fs.maybeChunkRange, p->getTrustedDensity) {
|
|
1805
|
+
| (Some(minHistoryRange), Some(density)) if density > 0. =>
|
|
1806
|
+
let chunkSize = Js.Math.ceil_int(minHistoryRange->Int.toFloat *. chunkRangeGrowthFactor)
|
|
1807
|
+
let maxChunksRemaining =
|
|
1808
|
+
maxInFlightChunksPerPartition - fs.inFlightCount - fs.chunksUsedThisCall
|
|
1809
|
+
// No chunk starts past chainTargetBlock; an emitted chunk still keeps
|
|
1810
|
+
// its full span (chunkToBlock may exceed the target — only
|
|
1811
|
+
// endBlock/mergeBlock are hard bounds).
|
|
1812
|
+
let chunkStartCeiling = Pervasives.min(maxBlock, chainTargetBlock)
|
|
1813
|
+
let created = ref(0)
|
|
1814
|
+
let chunkFromBlock = ref(fs.cursor)
|
|
1815
|
+
// Stop once this partition alone has generated more than the whole fresh
|
|
1816
|
+
// budget: the acceptance pass can hand a single partition at most the
|
|
1817
|
+
// budget plus one overshoot query, so further chunks could never be
|
|
1818
|
+
// accepted. Bounds generation (and the candidate sort) when the budget
|
|
1819
|
+
// is small relative to the pipeline cap.
|
|
1820
|
+
let generatedItems = ref(0.)
|
|
1821
|
+
while (
|
|
1822
|
+
created.contents < maxChunksRemaining &&
|
|
1823
|
+
chunkFromBlock.contents <= chunkStartCeiling &&
|
|
1824
|
+
generatedItems.contents <= freshBudget
|
|
1825
|
+
) {
|
|
1826
|
+
let chunkToBlock = Pervasives.min(chunkFromBlock.contents + chunkSize - 1, maxBlock)
|
|
1827
|
+
let itemsEst =
|
|
1828
|
+
candidates->pushDensityPricedQuery(
|
|
1829
|
+
~partitionId=fs.partitionId,
|
|
1830
|
+
~fromBlock=chunkFromBlock.contents,
|
|
1831
|
+
~toBlock=Some(chunkToBlock),
|
|
1832
|
+
~isChunk=true,
|
|
1833
|
+
~density,
|
|
1834
|
+
~chunkItemsMultiplier,
|
|
1835
|
+
~chainTargetBlock,
|
|
1836
|
+
~itemsTargetFloor,
|
|
1837
|
+
~selection=p.selection,
|
|
1838
|
+
~addressesByContractName=p.addressesByContractName,
|
|
1839
|
+
)
|
|
1840
|
+
generatedItems := generatedItems.contents +. itemsEst->Int.toFloat
|
|
1841
|
+
chunkFromBlock := chunkToBlock + 1
|
|
1842
|
+
created := created.contents + 1
|
|
1843
|
+
}
|
|
1844
|
+
| _ =>
|
|
1845
|
+
// Size the probe by the events its range to the target is expected to
|
|
1846
|
+
// hold — rangeTargetDensity × (chainTargetBlock − fromBlock + 1), split
|
|
1847
|
+
// across the partitions fetching this tick. With no range to the target
|
|
1848
|
+
// fall back to an even share of the fresh budget, so cold chains and
|
|
1849
|
+
// caught-up partitions still probe.
|
|
1850
|
+
let itemsTarget = if rangeToTarget > 0 {
|
|
1851
|
+
Pervasives.max(
|
|
1852
|
+
1,
|
|
1853
|
+
Math.round(
|
|
1854
|
+
rangeTargetDensity *.
|
|
1855
|
+
(chainTargetBlock - fs.cursor + 1)->Int.toFloat /.
|
|
1856
|
+
inRangeCount->Int.toFloat,
|
|
1857
|
+
)->Float.toInt,
|
|
1858
|
+
)
|
|
1859
|
+
} else {
|
|
1860
|
+
Pervasives.max(1, Math.round(probeShare)->Float.toInt)
|
|
1861
|
+
}
|
|
1862
|
+
candidates
|
|
1863
|
+
->Array.push({
|
|
1864
|
+
partitionId: fs.partitionId,
|
|
1865
|
+
fromBlock: fs.cursor,
|
|
1866
|
+
toBlock: fs.queryEndBlock,
|
|
1867
|
+
isChunk: false,
|
|
1868
|
+
selection: p.selection,
|
|
1869
|
+
itemsTarget,
|
|
1870
|
+
itemsEst: itemsTarget,
|
|
1871
|
+
addressesByContractName: p.addressesByContractName,
|
|
1872
|
+
})
|
|
1873
|
+
->ignore
|
|
1874
|
+
}
|
|
1875
|
+
})
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
// Acceptance: merge fresh candidates (Some) with the in-flight reservations
|
|
1879
|
+
// (None) and walk them in fromBlock order, starting from the full
|
|
1880
|
+
// chainTargetItems. A reservation just draws down the budget — its query is
|
|
1881
|
+
// already sent — while a candidate draws down the budget and is emitted.
|
|
1882
|
+
// Because a gap-fill's fromBlock precedes the in-flight query it unblocks,
|
|
1883
|
+
// it claims budget ahead of that reservation, so the buffer never deadlocks
|
|
1884
|
+
// waiting on a hole it can't fund. The candidate that tips the budget
|
|
1885
|
+
// negative is still emitted (a single overshoot); everything after it waits
|
|
1886
|
+
// for a tick with more budget. Accepted queries route back to their
|
|
1887
|
+
// partition bucket, so the output stays in idsInAscOrder with each
|
|
1888
|
+
// partition's queries in fromBlock order.
|
|
1889
|
+
let acceptCandidates = (
|
|
1890
|
+
~candidates: array<query>,
|
|
1891
|
+
~reservations: array<(int, int)>,
|
|
1892
|
+
~chainTargetItems: float,
|
|
1893
|
+
~partitionIndexById: dict<int>,
|
|
1894
|
+
~queriesByPartitionIndex: array<array<query>>,
|
|
1895
|
+
) => {
|
|
1896
|
+
let acceptanceStream = []
|
|
1897
|
+
candidates->Array.forEach(query =>
|
|
1898
|
+
acceptanceStream->Array.push((query.fromBlock, query.itemsEst, Some(query)))->ignore
|
|
1899
|
+
)
|
|
1900
|
+
reservations->Array.forEach(((fromBlock, itemsEst)) =>
|
|
1901
|
+
acceptanceStream->Array.push((fromBlock, itemsEst, None))->ignore
|
|
1902
|
+
)
|
|
1903
|
+
// Sort by fromBlock; on a tie charge the in-flight reservation (None) before
|
|
1904
|
+
// a fresh candidate (Some), so a same-block candidate can't overshoot the
|
|
1905
|
+
// target buffer. Only a strictly-earlier candidate — a gap-fill, whose
|
|
1906
|
+
// fromBlock precedes the query it unblocks — borrows ahead of a reservation.
|
|
1907
|
+
acceptanceStream->Array.sort(((aFrom, _, aQuery), (bFrom, _, bQuery)) =>
|
|
1908
|
+
if aFrom !== bFrom {
|
|
1909
|
+
Int.compare(aFrom, bFrom)
|
|
1910
|
+
} else {
|
|
1911
|
+
switch (aQuery, bQuery) {
|
|
1912
|
+
| (None, Some(_)) => Ordering.less
|
|
1913
|
+
| (Some(_), None) => Ordering.greater
|
|
1914
|
+
| (None, None) | (Some(_), Some(_)) => Ordering.equal
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
)
|
|
1918
|
+
let streamCount = acceptanceStream->Array.length
|
|
1919
|
+
let remainingBudget = ref(chainTargetItems)
|
|
1920
|
+
let acceptIdx = ref(0)
|
|
1921
|
+
// In-flight queries count against the chain's concurrency cap alongside the
|
|
1922
|
+
// ones accepted this tick; once the cap is reached no later candidate can be
|
|
1923
|
+
// accepted (they're only later in fromBlock order), so the walk stops.
|
|
1924
|
+
let usedConcurrency = ref(reservations->Array.length)
|
|
1925
|
+
while (
|
|
1926
|
+
remainingBudget.contents > 0. &&
|
|
1927
|
+
acceptIdx.contents < streamCount &&
|
|
1928
|
+
usedConcurrency.contents < maxChainConcurrency
|
|
1929
|
+
) {
|
|
1930
|
+
let (_, itemsEst, maybeQuery) = acceptanceStream->Array.getUnsafe(acceptIdx.contents)
|
|
1931
|
+
switch maybeQuery {
|
|
1932
|
+
| Some(query) =>
|
|
1933
|
+
let partitionIdx = partitionIndexById->Dict.getUnsafe(query.partitionId)
|
|
1934
|
+
queriesByPartitionIndex->Array.getUnsafe(partitionIdx)->Array.push(query)->ignore
|
|
1935
|
+
usedConcurrency := usedConcurrency.contents + 1
|
|
1936
|
+
| None => ()
|
|
1937
|
+
}
|
|
1938
|
+
remainingBudget := remainingBudget.contents -. itemsEst->Int.toFloat
|
|
1939
|
+
acceptIdx := acceptIdx.contents + 1
|
|
1940
|
+
}
|
|
1494
1941
|
}
|
|
1495
1942
|
|
|
1496
1943
|
// Candidate queries are sized against chainTargetBlock, the soft querying
|
|
@@ -1501,28 +1948,33 @@ type waterFillState = {
|
|
|
1501
1948
|
// query with no other ceiling: the true hard bounds stay
|
|
1502
1949
|
// endBlock/mergeBlock/the lagged head.
|
|
1503
1950
|
//
|
|
1504
|
-
//
|
|
1505
|
-
//
|
|
1506
|
-
//
|
|
1507
|
-
//
|
|
1508
|
-
//
|
|
1509
|
-
//
|
|
1510
|
-
//
|
|
1511
|
-
//
|
|
1512
|
-
//
|
|
1951
|
+
// The tick's budget is chainTargetItems minus what's already in flight. A
|
|
1952
|
+
// non-positive budget only resolves the wait action and generates no query
|
|
1953
|
+
// candidates. With a positive budget, every candidate query — gap-fill holes,
|
|
1954
|
+
// plus each in-range partition's chunks or open-ended probe toward the target —
|
|
1955
|
+
// is generated with no budget check, then the candidates are sorted by
|
|
1956
|
+
// fromBlock and accepted in that order while the budget stays positive. The
|
|
1957
|
+
// query that tips it negative is still accepted (a single overshoot);
|
|
1958
|
+
// everything after it waits for a tick with more budget.
|
|
1959
|
+
// Sorting by fromBlock spends the budget on the earliest blocks across all
|
|
1960
|
+
// partitions first, so no partition is starved by generation order and the
|
|
1961
|
+
// frontier advances evenly. In-flight reservations release as responses land,
|
|
1962
|
+
// so acceptance redistributes across ticks.
|
|
1513
1963
|
//
|
|
1514
|
-
// A partition with a
|
|
1515
|
-
//
|
|
1516
|
-
//
|
|
1517
|
-
//
|
|
1518
|
-
//
|
|
1519
|
-
//
|
|
1520
|
-
// density-0 estimate) emits one open-ended probe sized exactly at its
|
|
1521
|
-
// allotment instead.
|
|
1964
|
+
// A partition with source-capacity history and a positive density generates
|
|
1965
|
+
// density-sized chunks toward the target. Any other partition (no signal, no
|
|
1966
|
+
// capacity history, or a density-0 estimate) generates one open-ended probe
|
|
1967
|
+
// sized to the events its range to the target is expected to hold —
|
|
1968
|
+
// rangeTargetDensity × (chainTargetBlock − fromBlock + 1) / inRangeCount — so
|
|
1969
|
+
// unknown-density partitions probe in parallel within one budget.
|
|
1522
1970
|
let getNextQuery = (
|
|
1523
1971
|
{optimizedPartitions, blockLag, latestOnBlockBlockNumber, knownHeight, endBlock}: t,
|
|
1524
1972
|
~chainTargetBlock: int,
|
|
1525
1973
|
~chainTargetItems: float,
|
|
1974
|
+
~chunkItemsMultiplier: float=1.,
|
|
1975
|
+
// Floor for bounded queries' server cap (see pushDensityPricedQuery) —
|
|
1976
|
+
// targetBufferSize / maxChainConcurrency from the cross-chain scheduler.
|
|
1977
|
+
~itemsTargetFloor: int=0,
|
|
1526
1978
|
) => {
|
|
1527
1979
|
let headBlockNumber = knownHeight - blockLag
|
|
1528
1980
|
if headBlockNumber <= 0 {
|
|
@@ -1539,12 +1991,40 @@ let getNextQuery = (
|
|
|
1539
1991
|
|
|
1540
1992
|
let partitionsCount = optimizedPartitions.idsInAscOrder->Array.length
|
|
1541
1993
|
|
|
1542
|
-
//
|
|
1543
|
-
//
|
|
1544
|
-
//
|
|
1994
|
+
// Single scan over every partition, regardless of this tick's budget:
|
|
1995
|
+
// waiting-for-new-block bookkeeping, in-flight accounting, and the
|
|
1996
|
+
// id → index routing the acceptance pass uses.
|
|
1997
|
+
//
|
|
1998
|
+
// In-flight means fetchedBlock === None: a query whose response already
|
|
1999
|
+
// landed has had its reservation released by ChainState even while it
|
|
2000
|
+
// lingers in mutPendingQueries behind an unfilled gap, so counting it would
|
|
2001
|
+
// understate the budget and hold a concurrency slot it no longer uses.
|
|
2002
|
+
let inFlightCounts = Utils.Array.jsArrayCreate(partitionsCount)
|
|
2003
|
+
// (fromBlock, itemsEst) of each still-in-flight query. The acceptance pass
|
|
2004
|
+
// merges these into the candidate stream and draws them down in fromBlock
|
|
2005
|
+
// order, so a gap-fill sitting before an in-flight query claims budget ahead
|
|
2006
|
+
// of it and the buffer unblocks without waiting for that query to return.
|
|
2007
|
+
let reservations = []
|
|
2008
|
+
// In-flight itemsEst summed over the reservations. Sizes fresh forward
|
|
2009
|
+
// work below.
|
|
2010
|
+
let chainReserved = ref(0.)
|
|
2011
|
+
// Position of each partition in idsInAscOrder, so an accepted query routes
|
|
2012
|
+
// back to its bucket and the output stays in idsInAscOrder.
|
|
2013
|
+
let partitionIndexById = Dict.make()
|
|
1545
2014
|
for idx in 0 to partitionsCount - 1 {
|
|
1546
2015
|
let partitionId = optimizedPartitions.idsInAscOrder->Array.getUnsafe(idx)
|
|
1547
2016
|
let p = optimizedPartitions.entities->Dict.getUnsafe(partitionId)
|
|
2017
|
+
partitionIndexById->Dict.set(partitionId, idx)
|
|
2018
|
+
let inFlightCount = ref(0)
|
|
2019
|
+
for pqIdx in 0 to p.mutPendingQueries->Array.length - 1 {
|
|
2020
|
+
let pq = p.mutPendingQueries->Array.getUnsafe(pqIdx)
|
|
2021
|
+
if pq.fetchedBlock === None {
|
|
2022
|
+
inFlightCount := inFlightCount.contents + 1
|
|
2023
|
+
chainReserved := chainReserved.contents +. pq.itemsEst->Int.toFloat
|
|
2024
|
+
reservations->Array.push((pq.fromBlock, pq.itemsEst))->ignore
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
inFlightCounts->Array.setUnsafe(idx, inFlightCount.contents)
|
|
1548
2028
|
if (
|
|
1549
2029
|
p.mutPendingQueries->Array.length > 0 || p.latestFetchedBlock.blockNumber < headBlockNumber
|
|
1550
2030
|
) {
|
|
@@ -1557,8 +2037,20 @@ let getNextQuery = (
|
|
|
1557
2037
|
}
|
|
1558
2038
|
}
|
|
1559
2039
|
|
|
1560
|
-
//
|
|
1561
|
-
|
|
2040
|
+
// Fresh queries the acceptance pass can still admit this tick.
|
|
2041
|
+
let availableConcurrency = maxChainConcurrency - reservations->Array.length
|
|
2042
|
+
|
|
2043
|
+
// A zero budget is an admission check: preserve the wait-state scan above,
|
|
2044
|
+
// but make every query-generation pass below empty. Caught-up chains also
|
|
2045
|
+
// skip those passes because their action is already known. Same when the
|
|
2046
|
+
// chain is at its concurrency cap — no candidate could be accepted.
|
|
2047
|
+
let partitionsCount =
|
|
2048
|
+
chainTargetItems <= 0. || shouldWaitForNewBlock.contents || availableConcurrency <= 0
|
|
2049
|
+
? 0
|
|
2050
|
+
: partitionsCount
|
|
2051
|
+
|
|
2052
|
+
// One bucket per partition, in idsInAscOrder order — gap-fill and the
|
|
2053
|
+
// budget pass both push into a partition's own bucket, so flattening at
|
|
1562
2054
|
// the end (see below) reproduces idsInAscOrder without a sort.
|
|
1563
2055
|
let queriesByPartitionIndex: array<
|
|
1564
2056
|
array<query>,
|
|
@@ -1576,236 +2068,87 @@ let getNextQuery = (
|
|
|
1576
2068
|
}
|
|
1577
2069
|
}
|
|
1578
2070
|
|
|
1579
|
-
//
|
|
1580
|
-
//
|
|
1581
|
-
//
|
|
1582
|
-
//
|
|
1583
|
-
//
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
cost.contents +. (p.mutPendingQueries->Array.getUnsafe(pqIdx)).itemsTarget->Int.toFloat
|
|
1593
|
-
}
|
|
1594
|
-
existingReservedByPartition->Dict.set(partitionId, cost.contents)
|
|
1595
|
-
chainReserved := chainReserved.contents +. cost.contents
|
|
1596
|
-
}
|
|
2071
|
+
// Every candidate query for this tick — gap-fill holes plus each in-range
|
|
2072
|
+
// partition's chunks/probe toward the target — generated with no budget
|
|
2073
|
+
// check, then merged with the in-flight reservations, sorted by fromBlock,
|
|
2074
|
+
// and accepted while the budget lasts (acceptCandidates). Selecting by
|
|
2075
|
+
// fromBlock spends the budget on the earliest blocks across all partitions
|
|
2076
|
+
// first, so no partition is starved by iteration order and the frontier
|
|
2077
|
+
// advances evenly.
|
|
2078
|
+
let candidates = []
|
|
2079
|
+
|
|
2080
|
+
// Each partition's equal-divide share of the tick's budget, used to price
|
|
2081
|
+
// unknown-density gap probes.
|
|
2082
|
+
let partitionBudget =
|
|
2083
|
+
partitionsCount == 0 ? 0. : chainTargetItems /. partitionsCount->Int.toFloat
|
|
1597
2084
|
|
|
1598
|
-
// Phase A: gap-fill. Walk each partition's pending queries once,
|
|
1599
|
-
// unconditionally filling any hole (e.g. from an out-of-order partial
|
|
1600
|
-
// chunk response) — uncapped, same as before this tick's water-fill was
|
|
1601
|
-
// introduced. This also determines each partition's post-gap cursor and
|
|
1602
|
-
// whether it's blocked on an unresolved single-shot query.
|
|
1603
2085
|
let fillStates = []
|
|
1604
|
-
let gapFillCost = ref(0.)
|
|
1605
2086
|
for idx in 0 to partitionsCount - 1 {
|
|
1606
2087
|
let partitionId = optimizedPartitions.idsInAscOrder->Array.getUnsafe(idx)
|
|
1607
2088
|
let p = optimizedPartitions.entities->Dict.getUnsafe(partitionId)
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
let beforeLen = bucket->Array.length
|
|
1622
|
-
let cost = pushGapFillQueries(
|
|
1623
|
-
bucket,
|
|
1624
|
-
~partitionId,
|
|
1625
|
-
~rangeFromBlock=cursor.contents,
|
|
1626
|
-
~rangeEndBlock=Utils.Math.minOptInt(Some(pq.fromBlock - 1), queryEndBlock),
|
|
1627
|
-
~knownHeight,
|
|
1628
|
-
~chainTargetBlock,
|
|
1629
|
-
~maybeChunkRange,
|
|
1630
|
-
~maxChunks=maxPendingChunksPerPartition - pendingCount - chunksUsedThisCall.contents,
|
|
1631
|
-
~partition=p,
|
|
1632
|
-
~partitionBudget=chainTargetItems /. partitionsCount->Int.toFloat,
|
|
1633
|
-
~selection=p.selection,
|
|
1634
|
-
~addressesByContractName=p.addressesByContractName,
|
|
1635
|
-
)
|
|
1636
|
-
chunksUsedThisCall := chunksUsedThisCall.contents + (bucket->Array.length - beforeLen)
|
|
1637
|
-
gapFillCost := gapFillCost.contents +. cost
|
|
1638
|
-
}
|
|
1639
|
-
switch pq {
|
|
1640
|
-
| {isChunk: true, toBlock: Some(toBlock), fetchedBlock: Some({blockNumber})}
|
|
1641
|
-
if blockNumber < toBlock =>
|
|
1642
|
-
cursor := blockNumber + 1
|
|
1643
|
-
| {isChunk: true, toBlock: Some(toBlock)} => cursor := toBlock + 1
|
|
1644
|
-
| _ => canContinue := false
|
|
1645
|
-
}
|
|
1646
|
-
pqIdx := pqIdx.contents + 1
|
|
1647
|
-
}
|
|
1648
|
-
|
|
1649
|
-
if canContinue.contents {
|
|
1650
|
-
fillStates
|
|
1651
|
-
->Array.push({
|
|
1652
|
-
partitionId,
|
|
1653
|
-
p,
|
|
1654
|
-
cursor: cursor.contents,
|
|
1655
|
-
chunksUsedThisCall: chunksUsedThisCall.contents,
|
|
1656
|
-
pendingCount,
|
|
1657
|
-
queryEndBlock,
|
|
1658
|
-
maybeChunkRange,
|
|
1659
|
-
bucket,
|
|
1660
|
-
})
|
|
1661
|
-
->ignore
|
|
2089
|
+
switch p->walkPartitionPending(
|
|
2090
|
+
~partitionId,
|
|
2091
|
+
~inFlightCount=inFlightCounts->Array.getUnsafe(idx),
|
|
2092
|
+
~candidates,
|
|
2093
|
+
~headBlockNumber,
|
|
2094
|
+
~chainTargetBlock,
|
|
2095
|
+
~chunkItemsMultiplier,
|
|
2096
|
+
~itemsTargetFloor,
|
|
2097
|
+
~partitionBudget,
|
|
2098
|
+
~queryEndBlock=computeQueryEndBlock(p),
|
|
2099
|
+
) {
|
|
2100
|
+
| Some(fillState) => fillStates->Array.push(fillState)->ignore
|
|
2101
|
+
| None => ()
|
|
1662
2102
|
}
|
|
1663
2103
|
}
|
|
1664
2104
|
|
|
1665
|
-
//
|
|
1666
|
-
//
|
|
1667
|
-
//
|
|
1668
|
-
|
|
1669
|
-
let rangeItemsTarget = Pervasives.max(
|
|
1670
|
-
0.,
|
|
1671
|
-
chainTargetItems -. chainReserved.contents -. gapFillCost.contents,
|
|
1672
|
-
)
|
|
1673
|
-
|
|
1674
|
-
// Each in-range partition's running footprint: existing in-flight plus any
|
|
1675
|
-
// gap-fill just pushed into its bucket. The water-fill levels every
|
|
1676
|
-
// partition up toward a shared line, so this seed is what a partition
|
|
1677
|
-
// starts the fill already holding.
|
|
1678
|
-
let reservedByPartition = Dict.make()
|
|
1679
|
-
fillStates->Array.forEach(fs => {
|
|
1680
|
-
let cost = ref(existingReservedByPartition->Dict.getUnsafe(fs.partitionId))
|
|
1681
|
-
for i in 0 to fs.bucket->Array.length - 1 {
|
|
1682
|
-
cost := cost.contents +. (fs.bucket->Array.getUnsafe(i)).itemsTarget->Int.toFloat
|
|
1683
|
-
}
|
|
1684
|
-
reservedByPartition->Dict.set(fs.partitionId, cost.contents)
|
|
1685
|
-
})
|
|
2105
|
+
// Budget for fresh forward work: chainTargetItems minus what's still in
|
|
2106
|
+
// flight. Sizes probes and bounds chunk generation; the acceptance pass
|
|
2107
|
+
// does the final budgeting against the full chainTargetItems.
|
|
2108
|
+
let freshBudget = Pervasives.max(0., chainTargetItems -. chainReserved.contents)
|
|
1686
2109
|
|
|
1687
|
-
let isInRange = (fs:
|
|
2110
|
+
let isInRange = (fs: partitionFillState) =>
|
|
1688
2111
|
fs.cursor <= chainTargetBlock &&
|
|
1689
2112
|
switch fs.queryEndBlock {
|
|
1690
2113
|
| Some(eb) => fs.cursor <= eb
|
|
1691
2114
|
| None => true
|
|
1692
2115
|
} &&
|
|
1693
|
-
fs.
|
|
2116
|
+
fs.inFlightCount + fs.chunksUsedThisCall < maxInFlightChunksPerPartition
|
|
1694
2117
|
|
|
1695
2118
|
let inRangeStates = fillStates->Array.filter(isInRange)
|
|
1696
|
-
|
|
1697
|
-
//
|
|
1698
|
-
//
|
|
1699
|
-
//
|
|
1700
|
-
//
|
|
1701
|
-
//
|
|
1702
|
-
//
|
|
1703
|
-
//
|
|
1704
|
-
// probe
|
|
1705
|
-
let
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
}
|
|
1711
|
-
switch (fs.maybeChunkRange, p->getTrustedDensity) {
|
|
1712
|
-
| (Some(minHistoryRange), Some(density)) if density > 0. =>
|
|
1713
|
-
// Chunking active: strict chunks with a hard endBlock, uncapped real
|
|
1714
|
-
// density — at least one full chunk this round even if budget falls
|
|
1715
|
-
// short.
|
|
1716
|
-
let chunkSize = Js.Math.ceil_int(minHistoryRange->Int.toFloat *. 1.8)
|
|
1717
|
-
let chunkCost = density *. chunkSize->Int.toFloat
|
|
1718
|
-
let maxChunksRemaining =
|
|
1719
|
-
maxPendingChunksPerPartition - fs.pendingCount - fs.chunksUsedThisCall
|
|
1720
|
-
let affordable = Math.floor(budget /. chunkCost)->Float.toInt
|
|
1721
|
-
let numChunks = Pervasives.max(1, Pervasives.min(affordable, maxChunksRemaining))
|
|
1722
|
-
let consumed = ref(0.)
|
|
1723
|
-
let created = ref(0)
|
|
1724
|
-
let chunkFromBlock = ref(fs.cursor)
|
|
1725
|
-
while created.contents < numChunks && chunkFromBlock.contents <= maxBlock {
|
|
1726
|
-
let chunkToBlock = Pervasives.min(chunkFromBlock.contents + chunkSize - 1, maxBlock)
|
|
1727
|
-
let itemsTarget = Pervasives.max(
|
|
1728
|
-
1,
|
|
1729
|
-
(density *. (chunkToBlock - chunkFromBlock.contents + 1)->Int.toFloat)
|
|
1730
|
-
->Math.ceil
|
|
1731
|
-
->Float.toInt,
|
|
1732
|
-
)
|
|
1733
|
-
fs.bucket->Array.push({
|
|
1734
|
-
partitionId: fs.partitionId,
|
|
1735
|
-
fromBlock: chunkFromBlock.contents,
|
|
1736
|
-
toBlock: Some(chunkToBlock),
|
|
1737
|
-
isChunk: true,
|
|
1738
|
-
selection: p.selection,
|
|
1739
|
-
itemsTarget,
|
|
1740
|
-
addressesByContractName: p.addressesByContractName,
|
|
1741
|
-
})
|
|
1742
|
-
consumed := consumed.contents +. itemsTarget->Int.toFloat
|
|
1743
|
-
chunkFromBlock := chunkToBlock + 1
|
|
1744
|
-
created := created.contents + 1
|
|
1745
|
-
}
|
|
1746
|
-
fs.cursor = chunkFromBlock.contents
|
|
1747
|
-
fs.chunksUsedThisCall = fs.chunksUsedThisCall + created.contents
|
|
1748
|
-
consumed.contents
|
|
1749
|
-
| _ =>
|
|
1750
|
-
let itemsTarget = Pervasives.max(1, Math.round(budget)->Float.toInt)
|
|
1751
|
-
fs.bucket->Array.push({
|
|
1752
|
-
partitionId: fs.partitionId,
|
|
1753
|
-
fromBlock: fs.cursor,
|
|
1754
|
-
toBlock: fs.queryEndBlock,
|
|
1755
|
-
isChunk: false,
|
|
1756
|
-
selection: p.selection,
|
|
1757
|
-
itemsTarget,
|
|
1758
|
-
addressesByContractName: p.addressesByContractName,
|
|
1759
|
-
})
|
|
1760
|
-
fs.cursor = maxBlock + 1
|
|
1761
|
-
fs.chunksUsedThisCall = fs.chunksUsedThisCall + 1
|
|
1762
|
-
itemsTarget->Int.toFloat
|
|
1763
|
-
}
|
|
2119
|
+
let inRangeCount = inRangeStates->Array.length
|
|
2120
|
+
// The acceptance pass admits at most availableConcurrency fresh queries, in
|
|
2121
|
+
// fromBlock order, and every kept partition contributes a candidate at its
|
|
2122
|
+
// own cursor — so a partition past the first availableConcurrency in cursor
|
|
2123
|
+
// order could only offer candidates behind at least that many earlier ones,
|
|
2124
|
+
// which the concurrency cap stops the walk from ever reaching. Dropping
|
|
2125
|
+
// those partitions up front skips pointless candidate generation. A pure
|
|
2126
|
+
// generation bound: sizing still divides by the full inRangeCount, so each
|
|
2127
|
+
// probe keeps its honest per-partition share of the budget.
|
|
2128
|
+
let inRangeStates = if inRangeCount > availableConcurrency {
|
|
2129
|
+
inRangeStates->Array.sort((a, b) => Int.compare(a.cursor, b.cursor))
|
|
2130
|
+
inRangeStates->Array.slice(~start=0, ~end=availableConcurrency)
|
|
2131
|
+
} else {
|
|
2132
|
+
inRangeStates
|
|
1764
2133
|
}
|
|
1765
2134
|
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
while notFilledPartitions.contents->Array.length > 0 && remainingBudget.contents > 0. {
|
|
1783
|
-
let level = waterLevel(
|
|
1784
|
-
~budget=remainingBudget.contents,
|
|
1785
|
-
~footprints=notFilledPartitions.contents->Array.map(fs =>
|
|
1786
|
-
reservedByPartition->Dict.getUnsafe(fs.partitionId)
|
|
1787
|
-
),
|
|
1788
|
-
)
|
|
1789
|
-
let next = []
|
|
1790
|
-
notFilledPartitions.contents->Array.forEach(fs => {
|
|
1791
|
-
let reserved = reservedByPartition->Dict.getUnsafe(fs.partitionId)
|
|
1792
|
-
let budget = level -. reserved
|
|
1793
|
-
if budget > 0. {
|
|
1794
|
-
let consumed = emitQueries(fs, ~budget)
|
|
1795
|
-
reservedByPartition->Dict.set(fs.partitionId, reserved +. consumed)
|
|
1796
|
-
remainingBudget := remainingBudget.contents -. consumed
|
|
1797
|
-
if fs->isInRange {
|
|
1798
|
-
next->Array.push(fs)->ignore
|
|
1799
|
-
}
|
|
1800
|
-
}
|
|
1801
|
-
})
|
|
1802
|
-
notFilledPartitions := next
|
|
1803
|
-
}
|
|
2135
|
+
candidates->pushForwardCandidates(
|
|
2136
|
+
~inRangeStates,
|
|
2137
|
+
~inRangeCount,
|
|
2138
|
+
~chainTargetBlock,
|
|
2139
|
+
~freshBudget,
|
|
2140
|
+
~chunkItemsMultiplier,
|
|
2141
|
+
~itemsTargetFloor,
|
|
2142
|
+
)
|
|
2143
|
+
|
|
2144
|
+
acceptCandidates(
|
|
2145
|
+
~candidates,
|
|
2146
|
+
~reservations,
|
|
2147
|
+
~chainTargetItems,
|
|
2148
|
+
~partitionIndexById,
|
|
2149
|
+
~queriesByPartitionIndex,
|
|
2150
|
+
)
|
|
1804
2151
|
|
|
1805
|
-
// Each partition pushed only into its own bucket (indexed by its
|
|
1806
|
-
// idsInAscOrder position), so flattening reproduces idsInAscOrder order
|
|
1807
|
-
// directly — no sort needed even though water-fill rounds interleave
|
|
1808
|
-
// across partitions.
|
|
1809
2152
|
let queries = queriesByPartitionIndex->Array.flat
|
|
1810
2153
|
|
|
1811
2154
|
if queries->Utils.Array.isEmpty {
|
|
@@ -1900,10 +2243,10 @@ let make = (
|
|
|
1900
2243
|
mergeBlock: None,
|
|
1901
2244
|
dynamicContract: None,
|
|
1902
2245
|
mutPendingQueries: [],
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
2246
|
+
sourceRangeCapacity: 0,
|
|
2247
|
+
prevSourceRangeCapacity: 0,
|
|
2248
|
+
eventDensity: None,
|
|
2249
|
+
latestSourceRangeCapacityUpdateBlock: 0,
|
|
1907
2250
|
})
|
|
1908
2251
|
}
|
|
1909
2252
|
|
|
@@ -2146,6 +2489,8 @@ let rollback = (fetchState: t, ~indexingAddresses: IndexingAddresses.t, ~targetB
|
|
|
2146
2489
|
),
|
|
2147
2490
|
}->updateInternal(
|
|
2148
2491
|
~optimizedPartitions,
|
|
2492
|
+
// Filtering the sorted buffer keeps it sorted and deduped.
|
|
2493
|
+
~mutItemsSorted=true,
|
|
2149
2494
|
~mutItems=fetchState.buffer->Array.filter(item =>
|
|
2150
2495
|
switch item {
|
|
2151
2496
|
| Event({blockNumber})
|
|
@@ -2221,13 +2566,15 @@ let isReadyToEnterReorgThreshold = ({endBlock, blockLag, buffer, knownHeight} as
|
|
|
2221
2566
|
buffer->Utils.Array.isEmpty
|
|
2222
2567
|
}
|
|
2223
2568
|
|
|
2224
|
-
// Lower progress percentage = further behind = higher priority.
|
|
2225
|
-
//
|
|
2569
|
+
// Lower progress percentage = further behind = higher priority. Progress is
|
|
2570
|
+
// relative to the head this chain can actually fetch, so a chain at its lagged
|
|
2571
|
+
// head does not look behind relative to unavailable blocks. Shared by the batch
|
|
2572
|
+
// ordering and the cross-chain fetch priority.
|
|
2226
2573
|
let getProgressPercentage = (fetchState: t) => {
|
|
2227
2574
|
switch fetchState.firstEventBlock {
|
|
2228
2575
|
| None => 0.
|
|
2229
2576
|
| Some(firstEventBlock) =>
|
|
2230
|
-
let totalRange = fetchState.knownHeight - firstEventBlock
|
|
2577
|
+
let totalRange = fetchState.knownHeight - fetchState.blockLag - firstEventBlock
|
|
2231
2578
|
if totalRange <= 0 {
|
|
2232
2579
|
0.
|
|
2233
2580
|
} else {
|