envio 3.3.0-alpha.8 → 3.3.0-rc.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 +205 -59
- package/src/ChainState.res.mjs +130 -27
- package/src/ChainState.resi +16 -1
- 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 +147 -39
- package/src/CrossChainState.res.mjs +42 -10
- 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/EnvioGlobal.res +53 -0
- package/src/EnvioGlobal.res.mjs +31 -0
- package/src/EventConfigBuilder.res +30 -56
- package/src/EventConfigBuilder.res.mjs +24 -30
- package/src/EventProcessing.res +8 -5
- package/src/EventProcessing.res.mjs +2 -1
- package/src/FetchState.res +842 -474
- package/src/FetchState.res.mjs +545 -356
- package/src/HandlerLoader.res +1 -1
- package/src/HandlerLoader.res.mjs +1 -1
- package/src/HandlerRegister.res +478 -305
- package/src/HandlerRegister.res.mjs +277 -209
- package/src/HandlerRegister.resi +11 -6
- 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 +121 -18
- 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 +92 -217
- package/src/LogSelection.res.mjs +95 -184
- package/src/Main.res +29 -143
- package/src/Main.res.mjs +25 -88
- 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/RollbackCommit.res +4 -1
- package/src/RollbackCommit.res.mjs +3 -2
- package/src/SimulateDeadInputTracker.res +1 -1
- package/src/SimulateItems.res +39 -7
- 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/Evm.res +4 -1
- package/src/sources/Evm.res.mjs +1 -1
- 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/Fuel.res +3 -1
- package/src/sources/Fuel.res.mjs +1 -1
- 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 -210
- package/src/sources/HyperSyncSource.res.mjs +9 -137
- package/src/sources/Rpc.res +0 -32
- package/src/sources/Rpc.res.mjs +1 -46
- package/src/sources/RpcSource.res +129 -533
- package/src/sources/RpcSource.res.mjs +161 -379
- package/src/sources/SimulateSource.res +37 -19
- package/src/sources/SimulateSource.res.mjs +27 -10
- package/src/sources/SourceManager.res +4 -14
- package/src/sources/SourceManager.res.mjs +3 -9
- 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
|
-
|
|
21
|
-
//
|
|
22
|
-
|
|
19
|
+
// Items this in-flight query is targeting (server maxNumLogs-style cap).
|
|
20
|
+
itemsTarget: int,
|
|
21
|
+
// Estimated items this in-flight query will actually return (no headroom),
|
|
22
|
+
// carried from the query so the shared buffer budget can account for what's
|
|
23
|
+
// already being fetched without the cap's safety margin inflating it.
|
|
24
|
+
itemsEst: int,
|
|
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
|
-
|
|
70
|
-
//
|
|
71
|
-
|
|
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.
|
|
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
|
}
|
|
@@ -91,45 +103,41 @@ let deriveContractNameByAddress: dict<array<Address.t>> => dict<
|
|
|
91
103
|
result
|
|
92
104
|
})
|
|
93
105
|
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
// owning chain wants to reach (see getNextQuery). A partition that responded
|
|
108
|
-
// with no items has density 0, so its queries cost 0 — correct, they don't
|
|
109
|
-
// fill the buffer. Only called for partitions with a known density
|
|
110
|
-
// (prevQueryRange > 0); partitions with no signal yet are sized instead
|
|
111
|
-
// against their even split of the chain's range budget.
|
|
112
|
-
let densityItemsTarget = (p: partition, ~fromBlock, ~toBlock, ~chainTargetBlock) => {
|
|
113
|
-
let density = p.prevRangeSize->Int.toFloat /. p.prevQueryRange->Int.toFloat
|
|
114
|
-
(toBlock->Option.getOr(chainTargetBlock) - fromBlock + 1)->Int.toFloat *. density
|
|
106
|
+
// itemsTarget for a query over [fromBlock, toBlock] at the given event density
|
|
107
|
+
// (items/block). toBlock None is the open-ended tail, capped at
|
|
108
|
+
// chainTargetBlock — the soft per-tick horizon the owning chain wants to reach
|
|
109
|
+
// (see getNextQuery).
|
|
110
|
+
let densityItemsTarget = (~density, ~fromBlock, ~toBlock, ~chainTargetBlock) => {
|
|
111
|
+
// Floor at 1: the reservation must equal the server-side cap SourceManager
|
|
112
|
+
// sends, and a 0 cap would ask the backend for nothing.
|
|
113
|
+
Pervasives.max(
|
|
114
|
+
1,
|
|
115
|
+
((toBlock->Option.getOr(chainTargetBlock) - fromBlock + 1)->Int.toFloat *. density)
|
|
116
|
+
->Math.ceil
|
|
117
|
+
->Float.toInt,
|
|
118
|
+
)
|
|
115
119
|
}
|
|
116
120
|
|
|
117
|
-
// Calculate the chunk range from
|
|
121
|
+
// Calculate the chunk range from the last two source-capacity observations.
|
|
118
122
|
let getMinHistoryRange = (p: partition) => {
|
|
119
|
-
switch (p.
|
|
123
|
+
switch (p.sourceRangeCapacity, p.prevSourceRangeCapacity) {
|
|
120
124
|
| (0, _) | (_, 0) => None
|
|
121
125
|
| (a, b) => Some(a < b ? a : b)
|
|
122
126
|
}
|
|
123
127
|
}
|
|
124
128
|
|
|
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
|
|
132
|
+
|
|
125
133
|
let getMinQueryRange = (partitions: array<partition>) => {
|
|
126
134
|
let min = ref(0)
|
|
127
135
|
for i in 0 to partitions->Array.length - 1 {
|
|
128
136
|
let p = partitions->Array.getUnsafe(i)
|
|
129
137
|
|
|
130
138
|
// Even if it's fetching, set dynamicContract field
|
|
131
|
-
let a = p.
|
|
132
|
-
let b = p.
|
|
139
|
+
let a = p.sourceRangeCapacity
|
|
140
|
+
let b = p.prevSourceRangeCapacity
|
|
133
141
|
if a > 0 && (min.contents == 0 || a < min.contents) {
|
|
134
142
|
min := a
|
|
135
143
|
}
|
|
@@ -201,6 +209,15 @@ module OptimizedPartitions = {
|
|
|
201
209
|
let newId = nextPartitionIndexRef.contents->Int.toString
|
|
202
210
|
nextPartitionIndexRef := nextPartitionIndexRef.contents + 1
|
|
203
211
|
let minRange = getMinQueryRange([p1, p2])
|
|
212
|
+
// The merged partition indexes both parents' addresses, so its expected
|
|
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
|
+
}
|
|
204
221
|
{
|
|
205
222
|
id: newId,
|
|
206
223
|
dynamicContract: Some(contractName),
|
|
@@ -209,10 +226,10 @@ module OptimizedPartitions = {
|
|
|
209
226
|
mergeBlock: None,
|
|
210
227
|
addressesByContractName: Dict.make(), // set below
|
|
211
228
|
mutPendingQueries: [],
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
229
|
+
sourceRangeCapacity: minRange,
|
|
230
|
+
prevSourceRangeCapacity: minRange,
|
|
231
|
+
eventDensity: inheritedDensity,
|
|
232
|
+
latestSourceRangeCapacityUpdateBlock: 0,
|
|
216
233
|
}
|
|
217
234
|
}
|
|
218
235
|
|
|
@@ -413,14 +430,19 @@ module OptimizedPartitions = {
|
|
|
413
430
|
) => {
|
|
414
431
|
let latestFetchedBlock = ref(initialLatestFetchedBlock)
|
|
415
432
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
|
424
446
|
}
|
|
425
447
|
|
|
426
448
|
latestFetchedBlock.contents
|
|
@@ -460,28 +482,51 @@ module OptimizedPartitions = {
|
|
|
460
482
|
pendingQuery.fetchedBlock = Some(latestFetchedBlock)
|
|
461
483
|
|
|
462
484
|
let blockRange = latestFetchedBlock.blockNumber - query.fromBlock + 1
|
|
463
|
-
//
|
|
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.
|
|
464
498
|
// Prevents degradation of the chunking heuristic when parallel query
|
|
465
499
|
// responses arrive out of order (e.g. earlier query with smaller range
|
|
466
500
|
// arriving after a later query with bigger range).
|
|
467
|
-
let
|
|
468
|
-
latestFetchedBlock.blockNumber > p.
|
|
501
|
+
let shouldUpdateSourceRangeCapacity =
|
|
502
|
+
latestFetchedBlock.blockNumber > p.latestSourceRangeCapacityUpdateBlock &&
|
|
469
503
|
switch query.toBlock {
|
|
470
|
-
| None =>
|
|
504
|
+
| None =>
|
|
505
|
+
// Don't update source capacity when very close to the head.
|
|
506
|
+
latestFetchedBlock.blockNumber < knownHeight - 10
|
|
471
507
|
| Some(queryToBlock) =>
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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.
|
|
477
518
|
switch getMinHistoryRange(p) {
|
|
478
519
|
| None => false // Chunking not active yet, don't update
|
|
479
520
|
| Some(minHistoryRange) => queryToBlock - query.fromBlock + 1 >= minHistoryRange
|
|
480
521
|
}
|
|
522
|
+
}
|
|
481
523
|
}
|
|
482
|
-
let
|
|
483
|
-
|
|
484
|
-
|
|
524
|
+
let updatedSourceRangeCapacity = shouldUpdateSourceRangeCapacity
|
|
525
|
+
? blockRange
|
|
526
|
+
: p.sourceRangeCapacity
|
|
527
|
+
let updatedPrevSourceRangeCapacity = shouldUpdateSourceRangeCapacity
|
|
528
|
+
? p.sourceRangeCapacity
|
|
529
|
+
: p.prevSourceRangeCapacity
|
|
485
530
|
|
|
486
531
|
// Process fetched queries from front of queue for main partition
|
|
487
532
|
let updatedLatestFetchedBlock = consumeFetchedQueries(
|
|
@@ -497,28 +542,65 @@ module OptimizedPartitions = {
|
|
|
497
542
|
|
|
498
543
|
if partitionReachedMergeBlock {
|
|
499
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
|
+
)
|
|
500
553
|
} else {
|
|
501
554
|
let updatedMainPartition = {
|
|
502
555
|
...p,
|
|
503
556
|
latestFetchedBlock: updatedLatestFetchedBlock,
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
557
|
+
sourceRangeCapacity: updatedSourceRangeCapacity,
|
|
558
|
+
prevSourceRangeCapacity: updatedPrevSourceRangeCapacity,
|
|
559
|
+
eventDensity: updatedEventDensity,
|
|
560
|
+
latestSourceRangeCapacityUpdateBlock: shouldUpdateSourceRangeCapacity
|
|
508
561
|
? latestFetchedBlock.blockNumber
|
|
509
|
-
: p.
|
|
562
|
+
: p.latestSourceRangeCapacityUpdateBlock,
|
|
510
563
|
}
|
|
511
564
|
|
|
512
565
|
mutEntities->Dict.set(p.id, updatedMainPartition)
|
|
513
|
-
}
|
|
514
566
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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
|
+
}
|
|
522
604
|
}
|
|
523
605
|
|
|
524
606
|
@inline
|
|
@@ -611,13 +693,90 @@ let bufferReadyCount = (fetchState: t) => {
|
|
|
611
693
|
/*
|
|
612
694
|
Comparitor for two events from the same chain. No need for chain id or timestamp
|
|
613
695
|
*/
|
|
614
|
-
let
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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
|
|
618
712
|
} else {
|
|
619
|
-
|
|
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
|
|
774
|
+
}
|
|
775
|
+
while j.contents < n {
|
|
776
|
+
push(newItems->Array.getUnsafe(j.contents))
|
|
777
|
+
j := j.contents + 1
|
|
620
778
|
}
|
|
779
|
+
merged
|
|
621
780
|
}
|
|
622
781
|
|
|
623
782
|
// Some big number which should be bigger than any log index
|
|
@@ -688,48 +847,46 @@ let updateInternal = (
|
|
|
688
847
|
fetchState: t,
|
|
689
848
|
~optimizedPartitions=fetchState.optimizedPartitions,
|
|
690
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,
|
|
691
854
|
~blockLag=fetchState.blockLag,
|
|
692
855
|
~knownHeight=fetchState.knownHeight,
|
|
693
856
|
): t => {
|
|
694
|
-
|
|
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
|
+
}
|
|
695
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 = []
|
|
696
867
|
let latestOnBlockBlockNumber = switch fetchState.onBlockRegistrations {
|
|
697
868
|
| [] => knownHeight
|
|
698
|
-
| onBlockRegistrations =>
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
|
708
|
-
|
|
|
709
|
-
}->Array.get(fetchState.maxOnBlockBufferSize - 1) {
|
|
710
|
-
| Some(item) => item->Internal.getItemBlockNumber
|
|
711
|
-
| None =>
|
|
712
|
-
switch optimizedPartitions->OptimizedPartitions.getLatestFullyFetchedBlock {
|
|
713
|
-
| None => knownHeight
|
|
714
|
-
| Some(latestFullyFetchedBlock) => latestFullyFetchedBlock.blockNumber
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
let mutItems = switch mutItemsRef.contents {
|
|
719
|
-
| Some(mutItems) => mutItems
|
|
720
|
-
| 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
|
|
721
880
|
}
|
|
722
|
-
mutItemsRef := Some(mutItems)
|
|
723
|
-
|
|
724
|
-
appendOnBlockItems(
|
|
725
|
-
~mutItems,
|
|
726
|
-
~onBlockRegistrations,
|
|
727
|
-
~indexerStartBlock=fetchState.startBlock,
|
|
728
|
-
~fromBlock=fetchState.latestOnBlockBlockNumber,
|
|
729
|
-
~maxBlockNumber,
|
|
730
|
-
~maxOnBlockBufferSize=fetchState.maxOnBlockBufferSize,
|
|
731
|
-
)
|
|
732
881
|
}
|
|
882
|
+
appendOnBlockItems(
|
|
883
|
+
~mutItems=blockItems,
|
|
884
|
+
~onBlockRegistrations,
|
|
885
|
+
~indexerStartBlock=fetchState.startBlock,
|
|
886
|
+
~fromBlock=fetchState.latestOnBlockBlockNumber,
|
|
887
|
+
~maxBlockNumber,
|
|
888
|
+
~maxOnBlockBufferSize=fetchState.maxOnBlockBufferSize,
|
|
889
|
+
)
|
|
733
890
|
}
|
|
734
891
|
|
|
735
892
|
let updatedFetchState = {
|
|
@@ -744,15 +901,10 @@ let updateInternal = (
|
|
|
744
901
|
latestOnBlockBlockNumber,
|
|
745
902
|
blockLag,
|
|
746
903
|
knownHeight,
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
| Some(mutItems) => {
|
|
752
|
-
mutItems->Array.sort(compareBufferItem)
|
|
753
|
-
mutItems
|
|
754
|
-
}
|
|
755
|
-
| 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)
|
|
756
908
|
},
|
|
757
909
|
firstEventBlock: fetchState.firstEventBlock,
|
|
758
910
|
}
|
|
@@ -898,10 +1050,10 @@ OptimizedPartitions.t => {
|
|
|
898
1050
|
addressesByContractName,
|
|
899
1051
|
mergeBlock: None,
|
|
900
1052
|
mutPendingQueries: [],
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
1053
|
+
sourceRangeCapacity: 0,
|
|
1054
|
+
prevSourceRangeCapacity: 0,
|
|
1055
|
+
eventDensity: None,
|
|
1056
|
+
latestSourceRangeCapacityUpdateBlock: 0,
|
|
905
1057
|
})
|
|
906
1058
|
nextPartitionIndexRef := nextPartitionIndexRef.contents + 1
|
|
907
1059
|
}
|
|
@@ -1234,10 +1386,10 @@ let registerDynamicContracts = (
|
|
|
1234
1386
|
addressesByContractName,
|
|
1235
1387
|
mergeBlock: None,
|
|
1236
1388
|
mutPendingQueries: p.mutPendingQueries,
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1389
|
+
sourceRangeCapacity: p.sourceRangeCapacity,
|
|
1390
|
+
prevSourceRangeCapacity: p.prevSourceRangeCapacity,
|
|
1391
|
+
eventDensity: p.eventDensity,
|
|
1392
|
+
latestSourceRangeCapacityUpdateBlock: p.latestSourceRangeCapacityUpdateBlock,
|
|
1241
1393
|
})
|
|
1242
1394
|
}
|
|
1243
1395
|
}
|
|
@@ -1269,6 +1421,26 @@ let registerDynamicContracts = (
|
|
|
1269
1421
|
}
|
|
1270
1422
|
}
|
|
1271
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
|
+
|
|
1272
1444
|
/*
|
|
1273
1445
|
Updates fetchState with a response for a given query.
|
|
1274
1446
|
Returns Error if the partition with given query cannot be found (unexpected)
|
|
@@ -1277,26 +1449,10 @@ newItems are ordered earliest to latest (as they are returned from the worker)
|
|
|
1277
1449
|
*/
|
|
1278
1450
|
let handleQueryResult = (
|
|
1279
1451
|
fetchState: t,
|
|
1280
|
-
~indexingAddresses: IndexingAddresses.t,
|
|
1281
1452
|
~query: query,
|
|
1282
1453
|
~latestFetchedBlock: blockNumberAndTimestamp,
|
|
1283
1454
|
~newItems,
|
|
1284
1455
|
): t => {
|
|
1285
|
-
// Drop events an address-param filter rejects. A merged partition may
|
|
1286
|
-
// over-fetch a wildcard event whose indexed address param references an
|
|
1287
|
-
// address registered after the log's block; `clientAddressFilter` is the
|
|
1288
|
-
// param-level analogue of EventRouter's srcAddress effectiveStartBlock check.
|
|
1289
|
-
let newItems = newItems->Array.filter(item =>
|
|
1290
|
-
switch item {
|
|
1291
|
-
| Internal.Event({onEventRegistration, payload, blockNumber}) =>
|
|
1292
|
-
switch onEventRegistration.clientAddressFilter {
|
|
1293
|
-
| Some(filter) =>
|
|
1294
|
-
filter(payload, blockNumber, indexingAddresses->IndexingAddresses.rawForFilter)
|
|
1295
|
-
| None => true
|
|
1296
|
-
}
|
|
1297
|
-
| _ => true
|
|
1298
|
-
}
|
|
1299
|
-
)
|
|
1300
1456
|
fetchState->updateInternal(
|
|
1301
1457
|
~optimizedPartitions=fetchState.optimizedPartitions->OptimizedPartitions.handleQueryResponse(
|
|
1302
1458
|
~query,
|
|
@@ -1304,10 +1460,14 @@ let handleQueryResult = (
|
|
|
1304
1460
|
~itemsCount=newItems->Array.length,
|
|
1305
1461
|
~latestFetchedBlock,
|
|
1306
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,
|
|
1307
1467
|
~mutItems=?{
|
|
1308
1468
|
switch newItems {
|
|
1309
1469
|
| [] => None
|
|
1310
|
-
| _ => Some(fetchState.buffer->
|
|
1470
|
+
| _ => Some(fetchState.buffer->mergeIntoBuffer(newItems))
|
|
1311
1471
|
}
|
|
1312
1472
|
},
|
|
1313
1473
|
)
|
|
@@ -1328,6 +1488,7 @@ let startFetchingQueries = ({optimizedPartitions}: t, ~queries: array<query>) =>
|
|
|
1328
1488
|
toBlock: q.toBlock,
|
|
1329
1489
|
isChunk: q.isChunk,
|
|
1330
1490
|
itemsTarget: q.itemsTarget,
|
|
1491
|
+
itemsEst: q.itemsEst,
|
|
1331
1492
|
fetchedBlock: None,
|
|
1332
1493
|
}
|
|
1333
1494
|
|
|
@@ -1349,54 +1510,129 @@ let startFetchingQueries = ({optimizedPartitions}: t, ~queries: array<query>) =>
|
|
|
1349
1510
|
}
|
|
1350
1511
|
|
|
1351
1512
|
// Most parallel in-flight chunk queries a single partition may have at once.
|
|
1352
|
-
|
|
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
|
+
}
|
|
1353
1578
|
|
|
1354
|
-
//
|
|
1355
|
-
// out-of-order partial response)
|
|
1356
|
-
//
|
|
1357
|
-
//
|
|
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.
|
|
1358
1590
|
let pushGapFillQueries = (
|
|
1359
1591
|
queries: array<query>,
|
|
1360
1592
|
~partitionId: string,
|
|
1361
1593
|
~rangeFromBlock: int,
|
|
1362
1594
|
~rangeEndBlock: option<int>,
|
|
1363
|
-
~
|
|
1595
|
+
~headBlockNumber: int,
|
|
1364
1596
|
~chainTargetBlock: int,
|
|
1365
1597
|
~maybeChunkRange: option<int>,
|
|
1366
1598
|
~maxChunks: int,
|
|
1367
1599
|
~partition: partition,
|
|
1600
|
+
~partitionBudget: float,
|
|
1601
|
+
~chunkItemsMultiplier: float,
|
|
1602
|
+
~itemsTargetFloor: int,
|
|
1368
1603
|
~selection: selection,
|
|
1369
1604
|
~addressesByContractName: dict<array<Address.t>>,
|
|
1370
1605
|
) => {
|
|
1371
|
-
|
|
1372
|
-
|
|
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 {
|
|
1373
1611
|
switch rangeEndBlock {
|
|
1374
1612
|
| Some(endBlock) if rangeFromBlock > endBlock => ()
|
|
1375
1613
|
| _ =>
|
|
1376
|
-
switch
|
|
1377
|
-
|
|
|
1378
|
-
|
|
1379
|
-
|
|
1614
|
+
let maxBlock = switch rangeEndBlock {
|
|
1615
|
+
| Some(eb) => eb
|
|
1616
|
+
| None => chainTargetBlock
|
|
1617
|
+
}
|
|
1618
|
+
let pushSingleQuery = (~density, ~isChunk) =>
|
|
1619
|
+
queries
|
|
1620
|
+
->pushDensityPricedQuery(
|
|
1621
|
+
~partitionId,
|
|
1380
1622
|
~fromBlock=rangeFromBlock,
|
|
1381
1623
|
~toBlock=rangeEndBlock,
|
|
1624
|
+
~isChunk,
|
|
1625
|
+
~density,
|
|
1626
|
+
~chunkItemsMultiplier,
|
|
1382
1627
|
~chainTargetBlock,
|
|
1628
|
+
~itemsTargetFloor,
|
|
1629
|
+
~selection,
|
|
1630
|
+
~addressesByContractName,
|
|
1383
1631
|
)
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
selection,
|
|
1389
|
-
isChunk: false,
|
|
1390
|
-
itemsTarget,
|
|
1391
|
-
addressesByContractName,
|
|
1392
|
-
})
|
|
1393
|
-
cost := cost.contents +. itemsTarget
|
|
1394
|
-
| Some(chunkRange) =>
|
|
1395
|
-
let maxBlock = switch rangeEndBlock {
|
|
1396
|
-
| Some(eb) => eb
|
|
1397
|
-
| None => chainTargetBlock
|
|
1398
|
-
}
|
|
1399
|
-
let chunkSize = Js.Math.ceil_int(chunkRange->Int.toFloat *. 1.8)
|
|
1632
|
+
->ignore
|
|
1633
|
+
switch (partition->getTrustedDensity, maybeChunkRange) {
|
|
1634
|
+
| (Some(density), Some(chunkRange)) if density > 0. =>
|
|
1635
|
+
let chunkSize = Js.Math.ceil_int(chunkRange->Int.toFloat *. chunkRangeGrowthFactor)
|
|
1400
1636
|
if rangeFromBlock + chunkSize * 2 - 1 <= maxBlock {
|
|
1401
1637
|
let chunkFromBlock = ref(rangeFromBlock)
|
|
1402
1638
|
let chunkIdx = ref(0)
|
|
@@ -1404,65 +1640,304 @@ let pushGapFillQueries = (
|
|
|
1404
1640
|
chunkIdx.contents < maxChunks && chunkFromBlock.contents + chunkSize - 1 <= maxBlock
|
|
1405
1641
|
) {
|
|
1406
1642
|
let chunkToBlock = chunkFromBlock.contents + chunkSize - 1
|
|
1407
|
-
|
|
1408
|
-
|
|
1643
|
+
queries
|
|
1644
|
+
->pushDensityPricedQuery(
|
|
1645
|
+
~partitionId,
|
|
1409
1646
|
~fromBlock=chunkFromBlock.contents,
|
|
1410
1647
|
~toBlock=Some(chunkToBlock),
|
|
1648
|
+
~isChunk=true,
|
|
1649
|
+
~density,
|
|
1650
|
+
~chunkItemsMultiplier,
|
|
1411
1651
|
~chainTargetBlock,
|
|
1652
|
+
~itemsTargetFloor,
|
|
1653
|
+
~selection,
|
|
1654
|
+
~addressesByContractName,
|
|
1412
1655
|
)
|
|
1413
|
-
|
|
1414
|
-
partitionId,
|
|
1415
|
-
fromBlock: chunkFromBlock.contents,
|
|
1416
|
-
toBlock: Some(chunkToBlock),
|
|
1417
|
-
isChunk: true,
|
|
1418
|
-
selection,
|
|
1419
|
-
itemsTarget,
|
|
1420
|
-
addressesByContractName,
|
|
1421
|
-
})
|
|
1422
|
-
cost := cost.contents +. itemsTarget
|
|
1656
|
+
->ignore
|
|
1423
1657
|
chunkFromBlock := chunkToBlock + 1
|
|
1424
1658
|
chunkIdx := chunkIdx.contents + 1
|
|
1425
1659
|
}
|
|
1426
1660
|
} else {
|
|
1427
1661
|
// Not enough room for 2 chunks, fall back to a single query
|
|
1428
|
-
|
|
1429
|
-
partition,
|
|
1430
|
-
~fromBlock=rangeFromBlock,
|
|
1431
|
-
~toBlock=rangeEndBlock,
|
|
1432
|
-
~chainTargetBlock,
|
|
1433
|
-
)
|
|
1434
|
-
queries->Array.push({
|
|
1435
|
-
partitionId,
|
|
1436
|
-
fromBlock: rangeFromBlock,
|
|
1437
|
-
toBlock: rangeEndBlock,
|
|
1438
|
-
selection,
|
|
1439
|
-
isChunk: rangeEndBlock !== None,
|
|
1440
|
-
itemsTarget,
|
|
1441
|
-
addressesByContractName,
|
|
1442
|
-
})
|
|
1443
|
-
cost := cost.contents +. itemsTarget
|
|
1662
|
+
pushSingleQuery(~density, ~isChunk=rangeEndBlock !== None)
|
|
1444
1663
|
}
|
|
1664
|
+
| (Some(density), None) if density > 0. => pushSingleQuery(~density, ~isChunk=false)
|
|
1665
|
+
| _ =>
|
|
1666
|
+
let remainingRange = Pervasives.max(1, chainTargetBlock - rangeFromBlock + 1)
|
|
1667
|
+
pushSingleQuery(~density=partitionBudget /. remainingRange->Int.toFloat, ~isChunk=false)
|
|
1445
1668
|
}
|
|
1446
1669
|
}
|
|
1447
1670
|
}
|
|
1448
|
-
cost.contents
|
|
1449
1671
|
}
|
|
1450
1672
|
|
|
1451
|
-
// Per-partition
|
|
1452
|
-
|
|
1673
|
+
// Per-partition state carried from the gap-fill/cursor walk to candidate
|
|
1674
|
+
// generation.
|
|
1675
|
+
type partitionFillState = {
|
|
1453
1676
|
partitionId: string,
|
|
1454
1677
|
p: partition,
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
//
|
|
1458
|
-
|
|
1459
|
-
|
|
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,
|
|
1460
1685
|
queryEndBlock: option<int>,
|
|
1461
1686
|
maybeChunkRange: option<int>,
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
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
|
+
}
|
|
1466
1941
|
}
|
|
1467
1942
|
|
|
1468
1943
|
// Candidate queries are sized against chainTargetBlock, the soft querying
|
|
@@ -1473,25 +1948,33 @@ type waterFillState = {
|
|
|
1473
1948
|
// query with no other ceiling: the true hard bounds stay
|
|
1474
1949
|
// endBlock/mergeBlock/the lagged head.
|
|
1475
1950
|
//
|
|
1476
|
-
//
|
|
1477
|
-
//
|
|
1478
|
-
//
|
|
1479
|
-
//
|
|
1480
|
-
//
|
|
1481
|
-
//
|
|
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.
|
|
1482
1963
|
//
|
|
1483
|
-
// A partition with
|
|
1484
|
-
//
|
|
1485
|
-
//
|
|
1486
|
-
//
|
|
1487
|
-
//
|
|
1488
|
-
//
|
|
1489
|
-
// response — a single sample is too noisy to size by) emits one query sized
|
|
1490
|
-
// exactly to its round's share 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.
|
|
1491
1970
|
let getNextQuery = (
|
|
1492
1971
|
{optimizedPartitions, blockLag, latestOnBlockBlockNumber, knownHeight, endBlock}: t,
|
|
1493
1972
|
~chainTargetBlock: int,
|
|
1494
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,
|
|
1495
1978
|
) => {
|
|
1496
1979
|
let headBlockNumber = knownHeight - blockLag
|
|
1497
1980
|
if headBlockNumber <= 0 {
|
|
@@ -1508,12 +1991,40 @@ let getNextQuery = (
|
|
|
1508
1991
|
|
|
1509
1992
|
let partitionsCount = optimizedPartitions.idsInAscOrder->Array.length
|
|
1510
1993
|
|
|
1511
|
-
//
|
|
1512
|
-
//
|
|
1513
|
-
//
|
|
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()
|
|
1514
2014
|
for idx in 0 to partitionsCount - 1 {
|
|
1515
2015
|
let partitionId = optimizedPartitions.idsInAscOrder->Array.getUnsafe(idx)
|
|
1516
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)
|
|
1517
2028
|
if (
|
|
1518
2029
|
p.mutPendingQueries->Array.length > 0 || p.latestFetchedBlock.blockNumber < headBlockNumber
|
|
1519
2030
|
) {
|
|
@@ -1526,8 +2037,20 @@ let getNextQuery = (
|
|
|
1526
2037
|
}
|
|
1527
2038
|
}
|
|
1528
2039
|
|
|
1529
|
-
//
|
|
1530
|
-
|
|
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
|
|
1531
2054
|
// the end (see below) reproduces idsInAscOrder without a sort.
|
|
1532
2055
|
let queriesByPartitionIndex: array<
|
|
1533
2056
|
array<query>,
|
|
@@ -1545,246 +2068,87 @@ let getNextQuery = (
|
|
|
1545
2068
|
}
|
|
1546
2069
|
}
|
|
1547
2070
|
|
|
1548
|
-
//
|
|
1549
|
-
//
|
|
1550
|
-
//
|
|
1551
|
-
//
|
|
1552
|
-
//
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
}
|
|
1562
|
-
existingReservedByPartition->Dict.set(partitionId, cost.contents)
|
|
1563
|
-
chainReserved := chainReserved.contents +. cost.contents
|
|
1564
|
-
}
|
|
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
|
|
1565
2084
|
|
|
1566
|
-
// Phase A: gap-fill. Walk each partition's pending queries once,
|
|
1567
|
-
// unconditionally filling any hole (e.g. from an out-of-order partial
|
|
1568
|
-
// chunk response) — uncapped, same as before this tick's water-fill was
|
|
1569
|
-
// introduced. This also determines each partition's post-gap cursor and
|
|
1570
|
-
// whether it's blocked on an unresolved single-shot query.
|
|
1571
2085
|
let fillStates = []
|
|
1572
|
-
let gapFillCost = ref(0.)
|
|
1573
2086
|
for idx in 0 to partitionsCount - 1 {
|
|
1574
2087
|
let partitionId = optimizedPartitions.idsInAscOrder->Array.getUnsafe(idx)
|
|
1575
2088
|
let p = optimizedPartitions.entities->Dict.getUnsafe(partitionId)
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
let beforeLen = bucket->Array.length
|
|
1590
|
-
let cost = pushGapFillQueries(
|
|
1591
|
-
bucket,
|
|
1592
|
-
~partitionId,
|
|
1593
|
-
~rangeFromBlock=cursor.contents,
|
|
1594
|
-
~rangeEndBlock=Utils.Math.minOptInt(Some(pq.fromBlock - 1), queryEndBlock),
|
|
1595
|
-
~knownHeight,
|
|
1596
|
-
~chainTargetBlock,
|
|
1597
|
-
~maybeChunkRange,
|
|
1598
|
-
~maxChunks=maxPendingChunksPerPartition - pendingCount - chunksUsedThisCall.contents,
|
|
1599
|
-
~partition=p,
|
|
1600
|
-
~selection=p.selection,
|
|
1601
|
-
~addressesByContractName=p.addressesByContractName,
|
|
1602
|
-
)
|
|
1603
|
-
chunksUsedThisCall := chunksUsedThisCall.contents + (bucket->Array.length - beforeLen)
|
|
1604
|
-
gapFillCost := gapFillCost.contents +. cost
|
|
1605
|
-
}
|
|
1606
|
-
switch pq {
|
|
1607
|
-
| {isChunk: true, toBlock: Some(toBlock), fetchedBlock: Some({blockNumber})}
|
|
1608
|
-
if blockNumber < toBlock =>
|
|
1609
|
-
cursor := blockNumber + 1
|
|
1610
|
-
| {isChunk: true, toBlock: Some(toBlock)} => cursor := toBlock + 1
|
|
1611
|
-
| _ => canContinue := false
|
|
1612
|
-
}
|
|
1613
|
-
pqIdx := pqIdx.contents + 1
|
|
1614
|
-
}
|
|
1615
|
-
|
|
1616
|
-
if canContinue.contents {
|
|
1617
|
-
fillStates
|
|
1618
|
-
->Array.push({
|
|
1619
|
-
partitionId,
|
|
1620
|
-
p,
|
|
1621
|
-
cursor: cursor.contents,
|
|
1622
|
-
chunksUsedThisCall: chunksUsedThisCall.contents,
|
|
1623
|
-
pendingCount,
|
|
1624
|
-
queryEndBlock,
|
|
1625
|
-
maybeChunkRange,
|
|
1626
|
-
bucket,
|
|
1627
|
-
})
|
|
1628
|
-
->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 => ()
|
|
1629
2102
|
}
|
|
1630
2103
|
}
|
|
1631
2104
|
|
|
1632
|
-
//
|
|
1633
|
-
//
|
|
1634
|
-
//
|
|
1635
|
-
|
|
1636
|
-
let rangeItemsTarget = Pervasives.max(
|
|
1637
|
-
0.,
|
|
1638
|
-
chainTargetItems -. chainReserved.contents -. gapFillCost.contents,
|
|
1639
|
-
)
|
|
1640
|
-
|
|
1641
|
-
// Each in-range partition's running footprint: existing in-flight plus any
|
|
1642
|
-
// gap-fill just pushed into its bucket. The water-fill levels every
|
|
1643
|
-
// partition up toward a shared line, so this seed is what a partition
|
|
1644
|
-
// starts the fill already holding.
|
|
1645
|
-
let reservedByPartition = Dict.make()
|
|
1646
|
-
fillStates->Array.forEach(fs => {
|
|
1647
|
-
let cost = ref(existingReservedByPartition->Dict.getUnsafe(fs.partitionId))
|
|
1648
|
-
for i in 0 to fs.bucket->Array.length - 1 {
|
|
1649
|
-
cost := cost.contents +. (fs.bucket->Array.getUnsafe(i)).itemsTarget
|
|
1650
|
-
}
|
|
1651
|
-
reservedByPartition->Dict.set(fs.partitionId, cost.contents)
|
|
1652
|
-
})
|
|
1653
|
-
|
|
1654
|
-
// Emits this round's queries for one partition, given its share of the
|
|
1655
|
-
// range budget. Mutates fs.cursor/chunksUsedThisCall and returns the
|
|
1656
|
-
// itemsTarget consumed.
|
|
1657
|
-
//
|
|
1658
|
-
// Density is trusted only once the chunking heuristic is active
|
|
1659
|
-
// (maybeChunkRange requires prevQueryRange AND prevPrevQueryRange, i.e.
|
|
1660
|
-
// two responses — see getMinHistoryRange) — a single response is noisy
|
|
1661
|
-
// (a first query that happens to land on an unusually dense/sparse block
|
|
1662
|
-
// would otherwise mis-size the very next one), so it's treated the same
|
|
1663
|
-
// as a partition with no signal at all: sized exactly to this round's
|
|
1664
|
-
// share instead of an uncapped, one-sample density estimate.
|
|
1665
|
-
let emitQueries = (fs: waterFillState, ~budget: float) => {
|
|
1666
|
-
let p = fs.p
|
|
1667
|
-
let maxBlock = switch fs.queryEndBlock {
|
|
1668
|
-
| Some(eb) => eb
|
|
1669
|
-
| None => chainTargetBlock
|
|
1670
|
-
}
|
|
1671
|
-
switch fs.maybeChunkRange {
|
|
1672
|
-
| Some(minHistoryRange) =>
|
|
1673
|
-
// Chunking active: strict chunks with a hard endBlock, uncapped real
|
|
1674
|
-
// density — at least one full chunk this round even if budget falls
|
|
1675
|
-
// short.
|
|
1676
|
-
let density = p.prevRangeSize->Int.toFloat /. p.prevQueryRange->Int.toFloat
|
|
1677
|
-
let chunkSize = Js.Math.ceil_int(minHistoryRange->Int.toFloat *. 1.8)
|
|
1678
|
-
let chunkCost = density *. chunkSize->Int.toFloat
|
|
1679
|
-
let maxChunksRemaining =
|
|
1680
|
-
maxPendingChunksPerPartition - fs.pendingCount - fs.chunksUsedThisCall
|
|
1681
|
-
let affordable = if chunkCost > 0. {
|
|
1682
|
-
Math.floor(budget /. chunkCost)->Float.toInt
|
|
1683
|
-
} else {
|
|
1684
|
-
maxChunksRemaining
|
|
1685
|
-
}
|
|
1686
|
-
let numChunks = Pervasives.max(1, Pervasives.min(affordable, maxChunksRemaining))
|
|
1687
|
-
let consumed = ref(0.)
|
|
1688
|
-
let created = ref(0)
|
|
1689
|
-
let chunkFromBlock = ref(fs.cursor)
|
|
1690
|
-
while created.contents < numChunks && chunkFromBlock.contents <= maxBlock {
|
|
1691
|
-
let chunkToBlock = Pervasives.min(chunkFromBlock.contents + chunkSize - 1, maxBlock)
|
|
1692
|
-
let itemsTarget = density *. (chunkToBlock - chunkFromBlock.contents + 1)->Int.toFloat
|
|
1693
|
-
fs.bucket->Array.push({
|
|
1694
|
-
partitionId: fs.partitionId,
|
|
1695
|
-
fromBlock: chunkFromBlock.contents,
|
|
1696
|
-
toBlock: Some(chunkToBlock),
|
|
1697
|
-
isChunk: true,
|
|
1698
|
-
selection: p.selection,
|
|
1699
|
-
itemsTarget,
|
|
1700
|
-
addressesByContractName: p.addressesByContractName,
|
|
1701
|
-
})
|
|
1702
|
-
consumed := consumed.contents +. itemsTarget
|
|
1703
|
-
chunkFromBlock := chunkToBlock + 1
|
|
1704
|
-
created := created.contents + 1
|
|
1705
|
-
}
|
|
1706
|
-
fs.cursor = chunkFromBlock.contents
|
|
1707
|
-
fs.chunksUsedThisCall = fs.chunksUsedThisCall + created.contents
|
|
1708
|
-
consumed.contents
|
|
1709
|
-
| None =>
|
|
1710
|
-
// No trusted density yet (zero or one response): one open query sized
|
|
1711
|
-
// to this round's share, capped so a single probe can't swallow the
|
|
1712
|
-
// whole (possibly cross-chain-wide) budget and starve other partitions
|
|
1713
|
-
// or chains before they get their own first probe.
|
|
1714
|
-
let itemsTarget = Pervasives.min(Math.round(budget), maxItemsTarget)
|
|
1715
|
-
fs.bucket->Array.push({
|
|
1716
|
-
partitionId: fs.partitionId,
|
|
1717
|
-
fromBlock: fs.cursor,
|
|
1718
|
-
toBlock: fs.queryEndBlock,
|
|
1719
|
-
isChunk: false,
|
|
1720
|
-
selection: p.selection,
|
|
1721
|
-
itemsTarget,
|
|
1722
|
-
addressesByContractName: p.addressesByContractName,
|
|
1723
|
-
})
|
|
1724
|
-
fs.cursor = maxBlock + 1
|
|
1725
|
-
fs.chunksUsedThisCall = fs.chunksUsedThisCall + 1
|
|
1726
|
-
itemsTarget
|
|
1727
|
-
}
|
|
1728
|
-
}
|
|
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)
|
|
1729
2109
|
|
|
1730
|
-
let isInRange = (fs:
|
|
2110
|
+
let isInRange = (fs: partitionFillState) =>
|
|
1731
2111
|
fs.cursor <= chainTargetBlock &&
|
|
1732
2112
|
switch fs.queryEndBlock {
|
|
1733
2113
|
| Some(eb) => fs.cursor <= eb
|
|
1734
2114
|
| None => true
|
|
1735
2115
|
} &&
|
|
1736
|
-
fs.
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
//
|
|
1741
|
-
//
|
|
1742
|
-
//
|
|
1743
|
-
//
|
|
1744
|
-
//
|
|
1745
|
-
//
|
|
1746
|
-
//
|
|
1747
|
-
//
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
// consuming fresh budget, so the not-filled set drains on its own and the
|
|
1754
|
-
// loop also stops once the whole budget is reserved.
|
|
1755
|
-
let notFilledPartitions = ref(fillStates->Array.filter(isInRange))
|
|
1756
|
-
let reservedFromRange = ref(0.)
|
|
1757
|
-
while (
|
|
1758
|
-
notFilledPartitions.contents->Array.length > 0 &&
|
|
1759
|
-
reservedFromRange.contents < rangeItemsTarget
|
|
1760
|
-
) {
|
|
1761
|
-
let n = notFilledPartitions.contents->Array.length
|
|
1762
|
-
let footprintSum = ref(0.)
|
|
1763
|
-
notFilledPartitions.contents->Array.forEach(fs =>
|
|
1764
|
-
footprintSum := footprintSum.contents +. reservedByPartition->Dict.getUnsafe(fs.partitionId)
|
|
1765
|
-
)
|
|
1766
|
-
let line =
|
|
1767
|
-
(rangeItemsTarget -. reservedFromRange.contents +. footprintSum.contents) /. n->Int.toFloat
|
|
1768
|
-
let next = []
|
|
1769
|
-
notFilledPartitions.contents->Array.forEach(fs => {
|
|
1770
|
-
let reserved = reservedByPartition->Dict.getUnsafe(fs.partitionId)
|
|
1771
|
-
let budget = line -. reserved
|
|
1772
|
-
if budget > 0. {
|
|
1773
|
-
let consumed = emitQueries(fs, ~budget)
|
|
1774
|
-
reservedByPartition->Dict.set(fs.partitionId, reserved +. consumed)
|
|
1775
|
-
reservedFromRange := reservedFromRange.contents +. consumed
|
|
1776
|
-
if fs->isInRange {
|
|
1777
|
-
next->Array.push(fs)->ignore
|
|
1778
|
-
}
|
|
1779
|
-
}
|
|
1780
|
-
})
|
|
1781
|
-
notFilledPartitions := next
|
|
2116
|
+
fs.inFlightCount + fs.chunksUsedThisCall < maxInFlightChunksPerPartition
|
|
2117
|
+
|
|
2118
|
+
let inRangeStates = fillStates->Array.filter(isInRange)
|
|
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
|
|
1782
2133
|
}
|
|
1783
2134
|
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
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
|
+
)
|
|
2151
|
+
|
|
1788
2152
|
let queries = queriesByPartitionIndex->Array.flat
|
|
1789
2153
|
|
|
1790
2154
|
if queries->Utils.Array.isEmpty {
|
|
@@ -1879,10 +2243,10 @@ let make = (
|
|
|
1879
2243
|
mergeBlock: None,
|
|
1880
2244
|
dynamicContract: None,
|
|
1881
2245
|
mutPendingQueries: [],
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
2246
|
+
sourceRangeCapacity: 0,
|
|
2247
|
+
prevSourceRangeCapacity: 0,
|
|
2248
|
+
eventDensity: None,
|
|
2249
|
+
latestSourceRangeCapacityUpdateBlock: 0,
|
|
1886
2250
|
})
|
|
1887
2251
|
}
|
|
1888
2252
|
|
|
@@ -2125,6 +2489,8 @@ let rollback = (fetchState: t, ~indexingAddresses: IndexingAddresses.t, ~targetB
|
|
|
2125
2489
|
),
|
|
2126
2490
|
}->updateInternal(
|
|
2127
2491
|
~optimizedPartitions,
|
|
2492
|
+
// Filtering the sorted buffer keeps it sorted and deduped.
|
|
2493
|
+
~mutItemsSorted=true,
|
|
2128
2494
|
~mutItems=fetchState.buffer->Array.filter(item =>
|
|
2129
2495
|
switch item {
|
|
2130
2496
|
| Event({blockNumber})
|
|
@@ -2200,13 +2566,15 @@ let isReadyToEnterReorgThreshold = ({endBlock, blockLag, buffer, knownHeight} as
|
|
|
2200
2566
|
buffer->Utils.Array.isEmpty
|
|
2201
2567
|
}
|
|
2202
2568
|
|
|
2203
|
-
// Lower progress percentage = further behind = higher priority.
|
|
2204
|
-
//
|
|
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.
|
|
2205
2573
|
let getProgressPercentage = (fetchState: t) => {
|
|
2206
2574
|
switch fetchState.firstEventBlock {
|
|
2207
2575
|
| None => 0.
|
|
2208
2576
|
| Some(firstEventBlock) =>
|
|
2209
|
-
let totalRange = fetchState.knownHeight - firstEventBlock
|
|
2577
|
+
let totalRange = fetchState.knownHeight - fetchState.blockLag - firstEventBlock
|
|
2210
2578
|
if totalRange <= 0 {
|
|
2211
2579
|
0.
|
|
2212
2580
|
} else {
|