envio 3.4.0 → 3.5.0-alpha.1
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/package.json +6 -6
- package/src/ChainState.res +8 -17
- package/src/ChainState.res.mjs +20 -8
- package/src/ChainState.resi +1 -7
- package/src/Config.res +9 -1
- package/src/Config.res.mjs +2 -1
- package/src/CrossChainState.res +2 -20
- package/src/CrossChainState.res.mjs +2 -6
- package/src/Env.res +16 -0
- package/src/Env.res.mjs +6 -0
- package/src/FetchState.res +369 -76
- package/src/FetchState.res.mjs +280 -68
- package/src/IndexingAddresses.res +67 -24
- package/src/IndexingAddresses.res.mjs +51 -22
- package/src/IndexingAddresses.resi +9 -4
- package/src/TestIndexer.res +8 -2
- package/src/TestIndexer.res.mjs +2 -2
- package/src/sources/EvmHyperSyncSource.res +3 -6
- package/src/sources/EvmHyperSyncSource.res.mjs +1 -1
- package/src/sources/EvmRpcClient.res +4 -0
- package/src/sources/HyperSync.res +3 -1
- package/src/sources/HyperSync.res.mjs +3 -2
- package/src/sources/HyperSync.resi +2 -1
- package/src/sources/HyperSyncClient.res +6 -1
- package/src/sources/RpcSource.res +57 -58
- package/src/sources/RpcSource.res.mjs +2 -1
- package/src/sources/Source.res +4 -2
- package/src/sources/SvmHyperSyncClient.res +2 -1
- package/src/sources/SvmHyperSyncSource.res +1 -1
package/src/FetchState.res
CHANGED
|
@@ -9,18 +9,27 @@ type blockNumberAndLogIndex = {blockNumber: int, logIndex: int}
|
|
|
9
9
|
|
|
10
10
|
type selection = {
|
|
11
11
|
onEventRegistrations: array<Internal.onEventRegistration>,
|
|
12
|
+
// Whether the partition's queries are built from its own address list
|
|
13
|
+
// (server-side address filtering). This is the *partition* property; not to
|
|
14
|
+
// be confused with the per-registration `Internal.onEventRegistration.dependsOnAddresses`.
|
|
12
15
|
dependsOnAddresses: bool,
|
|
16
|
+
// Contract names this query fetches address-free even though their
|
|
17
|
+
// registrations depend on addresses. The source passes these to the client so
|
|
18
|
+
// their log selections carry no server-side address filter and routing accepts
|
|
19
|
+
// any emitter; the JS `clientAddressFilter` still gates each item by registered
|
|
20
|
+
// address + effectiveStartBlock. Absent for normal partitions.
|
|
21
|
+
clientFilteredContracts?: array<string>,
|
|
13
22
|
}
|
|
14
23
|
|
|
15
24
|
type pendingQuery = {
|
|
16
25
|
fromBlock: int,
|
|
17
26
|
toBlock: option<int>,
|
|
18
27
|
isChunk: bool,
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
//
|
|
23
|
-
//
|
|
28
|
+
// Server maxNumLogs-style cap for this in-flight query. None for bounded
|
|
29
|
+
// chunk/gap-fill queries, which send no cap (their toBlock is the bound).
|
|
30
|
+
itemsTarget: option<int>,
|
|
31
|
+
// Estimated items this in-flight query will return, carried from the query so
|
|
32
|
+
// the shared buffer budget can account for what's already being fetched.
|
|
24
33
|
itemsEst: int,
|
|
25
34
|
// Stores latestFetchedBlock when query completes. Only needed to persist
|
|
26
35
|
// timestamp while earlier queries are still pending before updating
|
|
@@ -72,14 +81,15 @@ type query = {
|
|
|
72
81
|
fromBlock: int,
|
|
73
82
|
toBlock: option<int>,
|
|
74
83
|
isChunk: bool,
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
//
|
|
82
|
-
//
|
|
84
|
+
// Server-side maxNumLogs-style cap. Some only for open-ended probes, whose
|
|
85
|
+
// range isn't otherwise bounded; None for bounded chunk/gap-fill queries,
|
|
86
|
+
// whose toBlock already bounds the response so a cap would only self-truncate
|
|
87
|
+
// (worse under client-side address filtering, which counts filtered-out items
|
|
88
|
+
// toward the cap).
|
|
89
|
+
itemsTarget: option<int>,
|
|
90
|
+
// Density × the query's block range for a known-density partition, the
|
|
91
|
+
// query's budget share otherwise. This is the unit the chain's per-tick
|
|
92
|
+
// budget is reserved/consumed in and that sizes every query.
|
|
83
93
|
itemsEst: int,
|
|
84
94
|
selection: selection,
|
|
85
95
|
addressesByContractName: dict<array<Address.t>>,
|
|
@@ -103,13 +113,13 @@ let deriveContractNameByAddress: dict<array<Address.t>> => dict<
|
|
|
103
113
|
result
|
|
104
114
|
})
|
|
105
115
|
|
|
106
|
-
//
|
|
116
|
+
// itemsEst for a query over [fromBlock, toBlock] at the given event density
|
|
107
117
|
// (items/block). toBlock None is the open-ended tail, capped at
|
|
108
118
|
// chainTargetBlock — the soft per-tick horizon the owning chain wants to reach
|
|
109
119
|
// (see getNextQuery).
|
|
110
120
|
let densityItemsTarget = (~density, ~fromBlock, ~toBlock, ~chainTargetBlock) => {
|
|
111
|
-
// Floor at 1: the reservation
|
|
112
|
-
//
|
|
121
|
+
// Floor at 1: this is the budget reservation, and for a probe also its server
|
|
122
|
+
// cap — a 0 cap would ask the backend for nothing.
|
|
113
123
|
Pervasives.max(
|
|
114
124
|
1,
|
|
115
125
|
((toBlock->Option.getOr(chainTargetBlock) - fromBlock + 1)->Int.toFloat *. density)
|
|
@@ -159,6 +169,12 @@ module OptimizedPartitions = {
|
|
|
159
169
|
// Tracks all contract names that have been dynamically added.
|
|
160
170
|
// Never reset - used to determine when to split existing partitions.
|
|
161
171
|
dynamicContracts: Utils.Set.t<string>,
|
|
172
|
+
// Contract names switched to client-side address filtering once their
|
|
173
|
+
// registered address count crossed the server-side threshold. Sticky for
|
|
174
|
+
// the run: their addresses live only in the index, their events are fetched
|
|
175
|
+
// by the single address-free partition, and new dynamic addresses get a
|
|
176
|
+
// bounded backfill up to its frontier instead of a standing partition.
|
|
177
|
+
clientFilteredContracts: Utils.Set.t<string>,
|
|
162
178
|
}
|
|
163
179
|
|
|
164
180
|
@inline
|
|
@@ -283,6 +299,7 @@ module OptimizedPartitions = {
|
|
|
283
299
|
~maxAddrInPartition,
|
|
284
300
|
~nextPartitionIndex: int,
|
|
285
301
|
~dynamicContracts: Utils.Set.t<string>,
|
|
302
|
+
~clientFilteredContracts: Utils.Set.t<string>,
|
|
286
303
|
) => {
|
|
287
304
|
let newPartitions = []
|
|
288
305
|
let mergingPartitions = Dict.make()
|
|
@@ -416,6 +433,7 @@ module OptimizedPartitions = {
|
|
|
416
433
|
maxAddrInPartition,
|
|
417
434
|
nextPartitionIndex: nextPartitionIndexRef.contents,
|
|
418
435
|
dynamicContracts,
|
|
436
|
+
clientFilteredContracts,
|
|
419
437
|
}
|
|
420
438
|
}
|
|
421
439
|
|
|
@@ -467,14 +485,37 @@ module OptimizedPartitions = {
|
|
|
467
485
|
}
|
|
468
486
|
}
|
|
469
487
|
|
|
470
|
-
let handleQueryResponse = (
|
|
488
|
+
let rec handleQueryResponse = (
|
|
471
489
|
optimizedPartitions: t,
|
|
472
490
|
~query,
|
|
473
491
|
~knownHeight,
|
|
474
492
|
~itemsCount,
|
|
475
493
|
~latestFetchedBlock: blockNumberAndTimestamp,
|
|
494
|
+
) =>
|
|
495
|
+
switch optimizedPartitions.entities->Utils.Dict.dangerouslyGetNonOption(query.partitionId) {
|
|
496
|
+
// The partition was absorbed into the address-free client-filtered partition
|
|
497
|
+
// while this query was in flight. Its items are still merged into the buffer
|
|
498
|
+
// by the caller (and deduped); there's no partition bookkeeping left to do,
|
|
499
|
+
// and its reservation is released by ChainState regardless.
|
|
500
|
+
| None => optimizedPartitions
|
|
501
|
+
| Some(p) =>
|
|
502
|
+
optimizedPartitions->handleQueryResponseForPartition(
|
|
503
|
+
~p,
|
|
504
|
+
~query,
|
|
505
|
+
~knownHeight,
|
|
506
|
+
~itemsCount,
|
|
507
|
+
~latestFetchedBlock,
|
|
508
|
+
)
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
and handleQueryResponseForPartition = (
|
|
512
|
+
optimizedPartitions: t,
|
|
513
|
+
~p: partition,
|
|
514
|
+
~query,
|
|
515
|
+
~knownHeight,
|
|
516
|
+
~itemsCount,
|
|
517
|
+
~latestFetchedBlock: blockNumberAndTimestamp,
|
|
476
518
|
) => {
|
|
477
|
-
let p = optimizedPartitions->getOrThrow(~partitionId=query.partitionId)
|
|
478
519
|
let mutEntities = optimizedPartitions.entities->Utils.Dict.shallowCopy
|
|
479
520
|
|
|
480
521
|
// Mark query as fetched
|
|
@@ -508,8 +549,12 @@ module OptimizedPartitions = {
|
|
|
508
549
|
if latestFetchedBlock.blockNumber < queryToBlock {
|
|
509
550
|
// Partial response is direct capacity evidence — unless it was
|
|
510
551
|
// truncated by our own itemsTarget cap: that reflects the
|
|
511
|
-
// reservation we asked for, not what the server could return.
|
|
512
|
-
|
|
552
|
+
// reservation we asked for, not what the server could return. A
|
|
553
|
+
// capless bounded query can only have been truncated by the source.
|
|
554
|
+
switch query.itemsTarget {
|
|
555
|
+
| None => true
|
|
556
|
+
| Some(itemsTarget) => itemsCount < itemsTarget
|
|
557
|
+
}
|
|
513
558
|
} else {
|
|
514
559
|
// A full response updates only when the query's intended range
|
|
515
560
|
// covers at least the partition's current chunk range — meaning it
|
|
@@ -549,6 +594,7 @@ module OptimizedPartitions = {
|
|
|
549
594
|
~maxAddrInPartition=optimizedPartitions.maxAddrInPartition,
|
|
550
595
|
~nextPartitionIndex=optimizedPartitions.nextPartitionIndex,
|
|
551
596
|
~dynamicContracts=optimizedPartitions.dynamicContracts,
|
|
597
|
+
~clientFilteredContracts=optimizedPartitions.clientFilteredContracts,
|
|
552
598
|
)
|
|
553
599
|
} else {
|
|
554
600
|
let updatedMainPartition = {
|
|
@@ -598,6 +644,7 @@ module OptimizedPartitions = {
|
|
|
598
644
|
~maxAddrInPartition=optimizedPartitions.maxAddrInPartition,
|
|
599
645
|
~nextPartitionIndex=optimizedPartitions.nextPartitionIndex,
|
|
600
646
|
~dynamicContracts=optimizedPartitions.dynamicContracts,
|
|
647
|
+
~clientFilteredContracts=optimizedPartitions.clientFilteredContracts,
|
|
601
648
|
)
|
|
602
649
|
}
|
|
603
650
|
}
|
|
@@ -638,6 +685,11 @@ type t = {
|
|
|
638
685
|
onBlockRegistrations: array<Internal.onBlockRegistration>,
|
|
639
686
|
knownHeight: int,
|
|
640
687
|
firstEventBlock: option<int>,
|
|
688
|
+
// Per-contract registered-address count past which a dynamic contract is
|
|
689
|
+
// switched to client-side filtering. None disables the switch
|
|
690
|
+
// (sources that can't filter client-side, e.g. SVM/Fuel), leaving every
|
|
691
|
+
// contract filtered server-side.
|
|
692
|
+
clientFilterAddressThreshold: option<int>,
|
|
641
693
|
}
|
|
642
694
|
|
|
643
695
|
@inline
|
|
@@ -907,6 +959,7 @@ let updateInternal = (
|
|
|
907
959
|
| blockItems => base->mergeIntoBuffer(blockItems)
|
|
908
960
|
},
|
|
909
961
|
firstEventBlock: fetchState.firstEventBlock,
|
|
962
|
+
clientFilterAddressThreshold: fetchState.clientFilterAddressThreshold,
|
|
910
963
|
}
|
|
911
964
|
|
|
912
965
|
updatedFetchState
|
|
@@ -946,6 +999,209 @@ let addressesByContractNameGetAll = (addressesByContractName: dict<array<Address
|
|
|
946
999
|
all
|
|
947
1000
|
}
|
|
948
1001
|
|
|
1002
|
+
// Move a contract to client-side address filtering, recording why.
|
|
1003
|
+
let addClientFilteredContract = (
|
|
1004
|
+
clientFilteredContracts: Utils.Set.t<string>,
|
|
1005
|
+
~contractName,
|
|
1006
|
+
~chainId,
|
|
1007
|
+
~addressCount,
|
|
1008
|
+
~threshold,
|
|
1009
|
+
) => {
|
|
1010
|
+
clientFilteredContracts->Utils.Set.add(contractName)->ignore
|
|
1011
|
+
Logging.createChild(
|
|
1012
|
+
~params={
|
|
1013
|
+
"chainId": chainId,
|
|
1014
|
+
"contractName": contractName,
|
|
1015
|
+
"addressCount": addressCount,
|
|
1016
|
+
"threshold": threshold,
|
|
1017
|
+
},
|
|
1018
|
+
)->Logging.childTrace(
|
|
1019
|
+
"Switching contract to client-side address filtering: registered address count crossed the server-side threshold.",
|
|
1020
|
+
)
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// Fold every client-filtered contract's server-side partitions into client-side
|
|
1024
|
+
// fetching without tearing down established state:
|
|
1025
|
+
// - The standing address-free partition (no mergeBlock) keeps its id, frontier,
|
|
1026
|
+
// in-flight queries and learned density. Only when a newly-switched contract
|
|
1027
|
+
// must be added does its selection change — under a fresh id, so in-flight
|
|
1028
|
+
// responses built from the old selection are orphaned instead of advancing
|
|
1029
|
+
// the frontier past ranges the new contract wasn't fetched for.
|
|
1030
|
+
// - Partitions absorbed below the standing frontier (single-contract dynamic
|
|
1031
|
+
// partitions and config partitions all of whose contracts are
|
|
1032
|
+
// client-filtered, plus any prior backfill) become one bounded backfill
|
|
1033
|
+
// partition covering [their min frontier, standing frontier]: getNextQuery
|
|
1034
|
+
// caps its queries at mergeBlock and handleQueryResponse deletes it on
|
|
1035
|
+
// arrival. The overlap it re-delivers is deduped by mergeIntoBuffer, and the
|
|
1036
|
+
// re-fetch doubles as history for freshly registered addresses: events
|
|
1037
|
+
// dropped before the address was registered now pass the clientAddressFilter.
|
|
1038
|
+
// - A partition mixing client-filtered and server-side contracts stays,
|
|
1039
|
+
// stripped of the client-filtered contracts' addresses — the address-free
|
|
1040
|
+
// partition covers those logs, so fetching them server-side too would only
|
|
1041
|
+
// produce duplicates. Its frontier bounds the backfill from below so the
|
|
1042
|
+
// stripped contracts lose no range.
|
|
1043
|
+
//
|
|
1044
|
+
// Registrations come from the address-free partitions plus `normalSelection`
|
|
1045
|
+
// filtered to client-filtered contracts, so coverage never depends on which
|
|
1046
|
+
// partitions happened to be absorbed (a stripped contract may have no
|
|
1047
|
+
// absorbable partition at all).
|
|
1048
|
+
let collapseClientFilteredContracts = (
|
|
1049
|
+
partitions: array<partition>,
|
|
1050
|
+
~clientFilteredContracts: Utils.Set.t<string>,
|
|
1051
|
+
~normalSelection: selection,
|
|
1052
|
+
~nextPartitionIndexRef: ref<int>,
|
|
1053
|
+
) => {
|
|
1054
|
+
if clientFilteredContracts->Utils.Set.size === 0 {
|
|
1055
|
+
partitions
|
|
1056
|
+
} else {
|
|
1057
|
+
let kept = []
|
|
1058
|
+
let standingRef = ref(None)
|
|
1059
|
+
let backfills = []
|
|
1060
|
+
let absorbedPartitions = []
|
|
1061
|
+
let strippedFrontiers = []
|
|
1062
|
+
|
|
1063
|
+
partitions->Array.forEach(p =>
|
|
1064
|
+
switch p {
|
|
1065
|
+
| {selection: {dependsOnAddresses: false}, mergeBlock: None}
|
|
1066
|
+
if standingRef.contents->Option.isNone =>
|
|
1067
|
+
standingRef := Some(p)
|
|
1068
|
+
| {selection: {dependsOnAddresses: false}, mergeBlock: Some(_)} =>
|
|
1069
|
+
backfills->Array.push(p)->ignore
|
|
1070
|
+
| {selection: {dependsOnAddresses: false}} => absorbedPartitions->Array.push(p)->ignore
|
|
1071
|
+
| _ =>
|
|
1072
|
+
let contractNames = p.addressesByContractName->Dict.keysToArray
|
|
1073
|
+
let clientFilteredNames =
|
|
1074
|
+
contractNames->Array.filter(c => clientFilteredContracts->Utils.Set.has(c))
|
|
1075
|
+
if clientFilteredNames->Utils.Array.isEmpty {
|
|
1076
|
+
kept->Array.push(p)->ignore
|
|
1077
|
+
} else if clientFilteredNames->Array.length === contractNames->Array.length {
|
|
1078
|
+
absorbedPartitions->Array.push(p)->ignore
|
|
1079
|
+
} else {
|
|
1080
|
+
let stripped = p.addressesByContractName->Utils.Dict.shallowCopy
|
|
1081
|
+
clientFilteredNames->Array.forEach(c => stripped->Utils.Dict.deleteInPlace(c))
|
|
1082
|
+
strippedFrontiers->Array.push(p.latestFetchedBlock)->ignore
|
|
1083
|
+
kept->Array.push({...p, addressesByContractName: stripped})->ignore
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
)
|
|
1087
|
+
|
|
1088
|
+
let selectionChanged = switch standingRef.contents {
|
|
1089
|
+
| Some(standing) =>
|
|
1090
|
+
standing.selection.clientFilteredContracts->Option.mapOr(0, Array.length) !==
|
|
1091
|
+
clientFilteredContracts->Utils.Set.size
|
|
1092
|
+
| None => true
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
if (
|
|
1096
|
+
absorbedPartitions->Utils.Array.isEmpty &&
|
|
1097
|
+
strippedFrontiers->Utils.Array.isEmpty &&
|
|
1098
|
+
!selectionChanged
|
|
1099
|
+
) {
|
|
1100
|
+
// Nothing to fold in and no newly-switched contract: leave the standing
|
|
1101
|
+
// partition (and any in-progress backfill) untouched.
|
|
1102
|
+
partitions
|
|
1103
|
+
} else {
|
|
1104
|
+
let minFrontierRef: ref<option<blockNumberAndTimestamp>> = ref(None)
|
|
1105
|
+
let considerFrontier = (b: blockNumberAndTimestamp) =>
|
|
1106
|
+
switch minFrontierRef.contents {
|
|
1107
|
+
| Some(m) if m.blockNumber <= b.blockNumber => ()
|
|
1108
|
+
| _ => minFrontierRef := Some(b)
|
|
1109
|
+
}
|
|
1110
|
+
absorbedPartitions->Array.forEach(p => considerFrontier(p.latestFetchedBlock))
|
|
1111
|
+
backfills->Array.forEach(p => considerFrontier(p.latestFetchedBlock))
|
|
1112
|
+
strippedFrontiers->Array.forEach(considerFrontier)
|
|
1113
|
+
|
|
1114
|
+
let regByIndex = Dict.make()
|
|
1115
|
+
let addRegs = (regs: array<Internal.onEventRegistration>) =>
|
|
1116
|
+
regs->Array.forEach(reg => regByIndex->Dict.set(reg.index->Int.toString, reg))
|
|
1117
|
+
switch standingRef.contents {
|
|
1118
|
+
| Some(standing) => addRegs(standing.selection.onEventRegistrations)
|
|
1119
|
+
| None => ()
|
|
1120
|
+
}
|
|
1121
|
+
absorbedPartitions->Array.forEach(p =>
|
|
1122
|
+
if !p.selection.dependsOnAddresses {
|
|
1123
|
+
addRegs(p.selection.onEventRegistrations)
|
|
1124
|
+
}
|
|
1125
|
+
)
|
|
1126
|
+
addRegs(
|
|
1127
|
+
normalSelection.onEventRegistrations->Array.filter(reg =>
|
|
1128
|
+
clientFilteredContracts->Utils.Set.has(reg.eventConfig.contractName)
|
|
1129
|
+
),
|
|
1130
|
+
)
|
|
1131
|
+
let newSelection = {
|
|
1132
|
+
dependsOnAddresses: false,
|
|
1133
|
+
onEventRegistrations: regByIndex->Dict.valuesToArray,
|
|
1134
|
+
clientFilteredContracts: clientFilteredContracts->Utils.Set.toArray,
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
switch standingRef.contents {
|
|
1138
|
+
| None =>
|
|
1139
|
+
// First switch: no standing partition to preserve, so create it at the
|
|
1140
|
+
// min frontier directly — the whole range above is unfetched for the
|
|
1141
|
+
// client-filtered side anyway.
|
|
1142
|
+
switch minFrontierRef.contents {
|
|
1143
|
+
| None => kept
|
|
1144
|
+
| Some(minFrontier) =>
|
|
1145
|
+
let minRange = getMinQueryRange(absorbedPartitions)
|
|
1146
|
+
let id = nextPartitionIndexRef.contents->Int.toString
|
|
1147
|
+
nextPartitionIndexRef := nextPartitionIndexRef.contents + 1
|
|
1148
|
+
kept
|
|
1149
|
+
->Array.push({
|
|
1150
|
+
id,
|
|
1151
|
+
latestFetchedBlock: minFrontier,
|
|
1152
|
+
selection: newSelection,
|
|
1153
|
+
addressesByContractName: Dict.make(),
|
|
1154
|
+
mergeBlock: None,
|
|
1155
|
+
dynamicContract: None,
|
|
1156
|
+
mutPendingQueries: [],
|
|
1157
|
+
sourceRangeCapacity: minRange,
|
|
1158
|
+
prevSourceRangeCapacity: minRange,
|
|
1159
|
+
eventDensity: None,
|
|
1160
|
+
latestSourceRangeCapacityUpdateBlock: 0,
|
|
1161
|
+
})
|
|
1162
|
+
->ignore
|
|
1163
|
+
kept
|
|
1164
|
+
}
|
|
1165
|
+
| Some(standing) =>
|
|
1166
|
+
let standingOut = if selectionChanged {
|
|
1167
|
+
let id = nextPartitionIndexRef.contents->Int.toString
|
|
1168
|
+
nextPartitionIndexRef := nextPartitionIndexRef.contents + 1
|
|
1169
|
+
{...standing, id, selection: newSelection, mutPendingQueries: []}
|
|
1170
|
+
} else {
|
|
1171
|
+
standing
|
|
1172
|
+
}
|
|
1173
|
+
kept->Array.push(standingOut)->ignore
|
|
1174
|
+
switch minFrontierRef.contents {
|
|
1175
|
+
| Some(minFrontier) if minFrontier.blockNumber < standing.latestFetchedBlock.blockNumber =>
|
|
1176
|
+
let id = nextPartitionIndexRef.contents->Int.toString
|
|
1177
|
+
nextPartitionIndexRef := nextPartitionIndexRef.contents + 1
|
|
1178
|
+
kept
|
|
1179
|
+
->Array.push({
|
|
1180
|
+
id,
|
|
1181
|
+
latestFetchedBlock: minFrontier,
|
|
1182
|
+
selection: newSelection,
|
|
1183
|
+
addressesByContractName: Dict.make(),
|
|
1184
|
+
mergeBlock: Some(standing.latestFetchedBlock.blockNumber),
|
|
1185
|
+
dynamicContract: None,
|
|
1186
|
+
mutPendingQueries: [],
|
|
1187
|
+
// Same query shape as the standing partition, so inherit its
|
|
1188
|
+
// learned range and density instead of probing from scratch.
|
|
1189
|
+
sourceRangeCapacity: standing.sourceRangeCapacity,
|
|
1190
|
+
prevSourceRangeCapacity: standing.prevSourceRangeCapacity,
|
|
1191
|
+
eventDensity: standing.eventDensity,
|
|
1192
|
+
latestSourceRangeCapacityUpdateBlock: 0,
|
|
1193
|
+
})
|
|
1194
|
+
->ignore
|
|
1195
|
+
// An absorbed frontier at/above the standing frontier needs no
|
|
1196
|
+
// backfill: the standing partition hasn't fetched past there yet.
|
|
1197
|
+
| _ => ()
|
|
1198
|
+
}
|
|
1199
|
+
kept
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
949
1205
|
/**
|
|
950
1206
|
Creates partitions from indexing addresses with two phases:
|
|
951
1207
|
Phase 1: Create per-contract-name partitions (smart grouping by startBlock)
|
|
@@ -956,6 +1212,7 @@ Returns OptimizedPartitions.t directly.
|
|
|
956
1212
|
let createPartitionsFromIndexingAddresses = (
|
|
957
1213
|
~registeringContractsByContract: dict<dict<indexingAddress>>,
|
|
958
1214
|
~dynamicContracts: Utils.Set.t<string>,
|
|
1215
|
+
~clientFilteredContracts: Utils.Set.t<string>,
|
|
959
1216
|
~normalSelection: selection,
|
|
960
1217
|
~maxAddrInPartition: int,
|
|
961
1218
|
~nextPartitionIndex: int,
|
|
@@ -1127,12 +1384,22 @@ OptimizedPartitions.t => {
|
|
|
1127
1384
|
|
|
1128
1385
|
let mergedPartitions = mergedNonDynamic->Array.concat(dynamicPartitions)
|
|
1129
1386
|
|
|
1130
|
-
// Final step: concat existing partitions with phase 1+2 result
|
|
1387
|
+
// Final step: concat existing partitions with phase 1+2 result, collapse any
|
|
1388
|
+
// client-filtered contracts into the single address-free partition, and optimize.
|
|
1389
|
+
let allPartitions =
|
|
1390
|
+
existingPartitions
|
|
1391
|
+
->Array.concat(mergedPartitions)
|
|
1392
|
+
->collapseClientFilteredContracts(
|
|
1393
|
+
~clientFilteredContracts,
|
|
1394
|
+
~normalSelection,
|
|
1395
|
+
~nextPartitionIndexRef,
|
|
1396
|
+
)
|
|
1131
1397
|
OptimizedPartitions.make(
|
|
1132
|
-
~partitions=
|
|
1398
|
+
~partitions=allPartitions,
|
|
1133
1399
|
~maxAddrInPartition,
|
|
1134
1400
|
~nextPartitionIndex=nextPartitionIndexRef.contents,
|
|
1135
1401
|
~dynamicContracts,
|
|
1402
|
+
~clientFilteredContracts,
|
|
1136
1403
|
)
|
|
1137
1404
|
}
|
|
1138
1405
|
|
|
@@ -1392,9 +1659,39 @@ let registerDynamicContracts = (
|
|
|
1392
1659
|
// Include no-events dcs so later batches detect conflicts against them.
|
|
1393
1660
|
indexingAddresses->IndexingAddresses.register(noEventsAddresses)
|
|
1394
1661
|
|
|
1662
|
+
// Switch any dynamic contract that has just crossed the server-side
|
|
1663
|
+
// address threshold to client-side filtering. Sticky: the set only grows, and
|
|
1664
|
+
// collapse in createPartitionsFromIndexingAddresses folds the contract's
|
|
1665
|
+
// partitions into the single address-free partition.
|
|
1666
|
+
// Clone the sticky set before mutating so this update owns its copy and
|
|
1667
|
+
// older fetchState snapshots keep theirs (Utils.Set.add mutates in place).
|
|
1668
|
+
let clientFilteredContracts = switch fetchState.clientFilterAddressThreshold {
|
|
1669
|
+
| Some(threshold) =>
|
|
1670
|
+
let clientFilteredContracts =
|
|
1671
|
+
fetchState.optimizedPartitions.clientFilteredContracts
|
|
1672
|
+
->Utils.Set.toArray
|
|
1673
|
+
->Utils.Set.fromArray
|
|
1674
|
+
dynamicContractsRef.contents
|
|
1675
|
+
->Utils.Set.toArray
|
|
1676
|
+
->Array.forEach(contractName => {
|
|
1677
|
+
let addressCount = indexingAddresses->IndexingAddresses.contractCount(~contractName)
|
|
1678
|
+
if !(clientFilteredContracts->Utils.Set.has(contractName)) && addressCount > threshold {
|
|
1679
|
+
clientFilteredContracts->addClientFilteredContract(
|
|
1680
|
+
~contractName,
|
|
1681
|
+
~chainId=fetchState.chainId,
|
|
1682
|
+
~addressCount,
|
|
1683
|
+
~threshold,
|
|
1684
|
+
)
|
|
1685
|
+
}
|
|
1686
|
+
})
|
|
1687
|
+
clientFilteredContracts
|
|
1688
|
+
| None => fetchState.optimizedPartitions.clientFilteredContracts
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1395
1691
|
let optimizedPartitions = createPartitionsFromIndexingAddresses(
|
|
1396
1692
|
~registeringContractsByContract,
|
|
1397
1693
|
~dynamicContracts=dynamicContractsRef.contents,
|
|
1694
|
+
~clientFilteredContracts,
|
|
1398
1695
|
~normalSelection=fetchState.normalSelection,
|
|
1399
1696
|
~maxAddrInPartition=fetchState.optimizedPartitions.maxAddrInPartition,
|
|
1400
1697
|
~nextPartitionIndex=fetchState.optimizedPartitions.nextPartitionIndex +
|
|
@@ -1419,9 +1716,16 @@ let filterByClientAddress = (
|
|
|
1419
1716
|
items->Array.filter(item =>
|
|
1420
1717
|
switch item {
|
|
1421
1718
|
| Internal.Event({payload, blockNumber}) as item =>
|
|
1422
|
-
|
|
1719
|
+
let reg = (item->Internal.castUnsafeEventItem).onEventRegistration
|
|
1720
|
+
switch reg.clientAddressFilter {
|
|
1423
1721
|
| Some(filter) =>
|
|
1424
|
-
filter(
|
|
1722
|
+
filter(
|
|
1723
|
+
payload,
|
|
1724
|
+
blockNumber,
|
|
1725
|
+
indexingAddresses->IndexingAddresses.forContract(
|
|
1726
|
+
~contractName=reg.eventConfig.contractName,
|
|
1727
|
+
),
|
|
1728
|
+
)
|
|
1425
1729
|
| None => true
|
|
1426
1730
|
}
|
|
1427
1731
|
| _ => true
|
|
@@ -1505,26 +1809,19 @@ let maxInFlightChunksPerPartition = 12
|
|
|
1505
1809
|
// Most parallel in-flight queries a single chain may have at once, across all
|
|
1506
1810
|
// its partitions. Bounds source load on chains with many partitions, where the
|
|
1507
1811
|
// per-partition cap alone would admit thousands of concurrent queries.
|
|
1508
|
-
let maxChainConcurrency =
|
|
1812
|
+
let maxChainConcurrency = Env.maxChainConcurrency
|
|
1509
1813
|
|
|
1510
1814
|
// Chunk spans grow by this factor over the smallest recently observed source
|
|
1511
1815
|
// range, so the pipeline keeps probing for more capacity instead of locking in
|
|
1512
1816
|
// the first measurement.
|
|
1513
1817
|
let chunkRangeGrowthFactor = 1.8
|
|
1514
1818
|
|
|
1515
|
-
// Push one density-priced query and return its itemsEst
|
|
1516
|
-
//
|
|
1517
|
-
//
|
|
1518
|
-
// range
|
|
1519
|
-
//
|
|
1520
|
-
// A
|
|
1521
|
-
// itemsTargetFloor: its range is already the hard bound on the response, so a
|
|
1522
|
-
// low density estimate shrinking the cap only buys self-truncated responses —
|
|
1523
|
-
// each one a wasted roundtrip that opens a gap and pollutes nothing but our
|
|
1524
|
-
// own pipeline. The floor is the indexer target split across the chain's
|
|
1525
|
-
// concurrency slots, so even every in-flight bounded query hitting its floored
|
|
1526
|
-
// cap at once overshoots the pool by at most ~one buffer target. Open-ended
|
|
1527
|
-
// queries keep the pure density cap — there it's the only bound at all.
|
|
1819
|
+
// Push one density-priced query and return its itemsEst, the density estimate
|
|
1820
|
+
// that sizes the query and the chain's budget reservation. These queries are
|
|
1821
|
+
// chunks and gap-fills: their toBlock is a tight bound sized to the source's
|
|
1822
|
+
// range capacity, so they send no server cap (itemsTarget None) — a cap would
|
|
1823
|
+
// only self-truncate the bounded range, worse under client-side address
|
|
1824
|
+
// filtering. A rare unbounded call caps at the estimate as its only bound.
|
|
1528
1825
|
let pushDensityPricedQuery = (
|
|
1529
1826
|
queries: array<query>,
|
|
1530
1827
|
~partitionId,
|
|
@@ -1532,19 +1829,11 @@ let pushDensityPricedQuery = (
|
|
|
1532
1829
|
~toBlock,
|
|
1533
1830
|
~isChunk,
|
|
1534
1831
|
~density,
|
|
1535
|
-
~chunkItemsMultiplier,
|
|
1536
1832
|
~chainTargetBlock,
|
|
1537
|
-
~itemsTargetFloor,
|
|
1538
1833
|
~selection,
|
|
1539
1834
|
~addressesByContractName,
|
|
1540
1835
|
) => {
|
|
1541
1836
|
let itemsEst = densityItemsTarget(~density, ~fromBlock, ~toBlock, ~chainTargetBlock)
|
|
1542
|
-
let itemsTarget = densityItemsTarget(
|
|
1543
|
-
~density=density *. chunkItemsMultiplier,
|
|
1544
|
-
~fromBlock,
|
|
1545
|
-
~toBlock,
|
|
1546
|
-
~chainTargetBlock,
|
|
1547
|
-
)
|
|
1548
1837
|
queries
|
|
1549
1838
|
->Array.push({
|
|
1550
1839
|
partitionId,
|
|
@@ -1553,8 +1842,8 @@ let pushDensityPricedQuery = (
|
|
|
1553
1842
|
selection,
|
|
1554
1843
|
isChunk,
|
|
1555
1844
|
itemsTarget: switch toBlock {
|
|
1556
|
-
| Some(_) =>
|
|
1557
|
-
| None =>
|
|
1845
|
+
| Some(_) => None
|
|
1846
|
+
| None => Some(itemsEst)
|
|
1558
1847
|
},
|
|
1559
1848
|
itemsEst,
|
|
1560
1849
|
addressesByContractName,
|
|
@@ -1585,8 +1874,6 @@ let pushGapFillQueries = (
|
|
|
1585
1874
|
~maxChunks: int,
|
|
1586
1875
|
~partition: partition,
|
|
1587
1876
|
~partitionBudget: float,
|
|
1588
|
-
~chunkItemsMultiplier: float,
|
|
1589
|
-
~itemsTargetFloor: int,
|
|
1590
1877
|
~selection: selection,
|
|
1591
1878
|
~addressesByContractName: dict<array<Address.t>>,
|
|
1592
1879
|
) => {
|
|
@@ -1610,9 +1897,7 @@ let pushGapFillQueries = (
|
|
|
1610
1897
|
~toBlock=rangeEndBlock,
|
|
1611
1898
|
~isChunk,
|
|
1612
1899
|
~density,
|
|
1613
|
-
~chunkItemsMultiplier,
|
|
1614
1900
|
~chainTargetBlock,
|
|
1615
|
-
~itemsTargetFloor,
|
|
1616
1901
|
~selection,
|
|
1617
1902
|
~addressesByContractName,
|
|
1618
1903
|
)
|
|
@@ -1634,9 +1919,7 @@ let pushGapFillQueries = (
|
|
|
1634
1919
|
~toBlock=Some(chunkToBlock),
|
|
1635
1920
|
~isChunk=true,
|
|
1636
1921
|
~density,
|
|
1637
|
-
~chunkItemsMultiplier,
|
|
1638
1922
|
~chainTargetBlock,
|
|
1639
|
-
~itemsTargetFloor,
|
|
1640
1923
|
~selection,
|
|
1641
1924
|
~addressesByContractName,
|
|
1642
1925
|
)
|
|
@@ -1684,8 +1967,6 @@ let walkPartitionPending = (
|
|
|
1684
1967
|
~candidates: array<query>,
|
|
1685
1968
|
~headBlockNumber: int,
|
|
1686
1969
|
~chainTargetBlock: int,
|
|
1687
|
-
~chunkItemsMultiplier: float,
|
|
1688
|
-
~itemsTargetFloor: int,
|
|
1689
1970
|
~partitionBudget: float,
|
|
1690
1971
|
~queryEndBlock: option<int>,
|
|
1691
1972
|
): option<partitionFillState> => {
|
|
@@ -1712,8 +1993,6 @@ let walkPartitionPending = (
|
|
|
1712
1993
|
~maxChunks=maxInFlightChunksPerPartition - inFlightCount - chunksUsedThisCall.contents,
|
|
1713
1994
|
~partition=p,
|
|
1714
1995
|
~partitionBudget,
|
|
1715
|
-
~chunkItemsMultiplier,
|
|
1716
|
-
~itemsTargetFloor,
|
|
1717
1996
|
~selection=p.selection,
|
|
1718
1997
|
~addressesByContractName=p.addressesByContractName,
|
|
1719
1998
|
)
|
|
@@ -1757,14 +2036,12 @@ let pushForwardCandidates = (
|
|
|
1757
2036
|
// bound, see getNextQuery.
|
|
1758
2037
|
~inRangeStates: array<partitionFillState>,
|
|
1759
2038
|
// The full in-range partition count, pre-truncation. Probe sizing divides by
|
|
1760
|
-
// this so each probe's itemsEst
|
|
1761
|
-
//
|
|
1762
|
-
//
|
|
2039
|
+
// this so each probe's itemsEst stays the honest per-partition share for
|
|
2040
|
+
// budget control — sizing by the (fewer) admittable queries would let every
|
|
2041
|
+
// accepted probe over-fetch its share.
|
|
1763
2042
|
~inRangeCount: int,
|
|
1764
2043
|
~chainTargetBlock: int,
|
|
1765
2044
|
~freshBudget: float,
|
|
1766
|
-
~chunkItemsMultiplier: float,
|
|
1767
|
-
~itemsTargetFloor: int,
|
|
1768
2045
|
) => {
|
|
1769
2046
|
// Even share of the fresh budget across the partitions actually fetching
|
|
1770
2047
|
// this tick (not every partition — so budget isn't stranded on ones below
|
|
@@ -1818,9 +2095,7 @@ let pushForwardCandidates = (
|
|
|
1818
2095
|
~toBlock=Some(chunkToBlock),
|
|
1819
2096
|
~isChunk=true,
|
|
1820
2097
|
~density,
|
|
1821
|
-
~chunkItemsMultiplier,
|
|
1822
2098
|
~chainTargetBlock,
|
|
1823
|
-
~itemsTargetFloor,
|
|
1824
2099
|
~selection=p.selection,
|
|
1825
2100
|
~addressesByContractName=p.addressesByContractName,
|
|
1826
2101
|
)
|
|
@@ -1834,7 +2109,7 @@ let pushForwardCandidates = (
|
|
|
1834
2109
|
// across the partitions fetching this tick. With no range to the target
|
|
1835
2110
|
// fall back to an even share of the fresh budget, so cold chains and
|
|
1836
2111
|
// caught-up partitions still probe.
|
|
1837
|
-
let
|
|
2112
|
+
let itemsEst = if rangeToTarget > 0 {
|
|
1838
2113
|
Pervasives.max(
|
|
1839
2114
|
1,
|
|
1840
2115
|
Math.round(
|
|
@@ -1853,8 +2128,10 @@ let pushForwardCandidates = (
|
|
|
1853
2128
|
toBlock: fs.queryEndBlock,
|
|
1854
2129
|
isChunk: false,
|
|
1855
2130
|
selection: p.selection,
|
|
1856
|
-
|
|
1857
|
-
|
|
2131
|
+
// An open-ended probe's range isn't bounded to source capacity, so it
|
|
2132
|
+
// keeps a server cap at its estimate to protect the shared buffer.
|
|
2133
|
+
itemsTarget: Some(itemsEst),
|
|
2134
|
+
itemsEst,
|
|
1858
2135
|
addressesByContractName: p.addressesByContractName,
|
|
1859
2136
|
})
|
|
1860
2137
|
->ignore
|
|
@@ -1958,10 +2235,6 @@ let getNextQuery = (
|
|
|
1958
2235
|
{optimizedPartitions, blockLag, latestOnBlockBlockNumber, knownHeight, endBlock}: t,
|
|
1959
2236
|
~chainTargetBlock: int,
|
|
1960
2237
|
~chainTargetItems: float,
|
|
1961
|
-
~chunkItemsMultiplier: float=1.,
|
|
1962
|
-
// Floor for bounded queries' server cap (see pushDensityPricedQuery) —
|
|
1963
|
-
// targetBufferSize / maxChainConcurrency from the cross-chain scheduler.
|
|
1964
|
-
~itemsTargetFloor: int=0,
|
|
1965
2238
|
) => {
|
|
1966
2239
|
let headBlockNumber = knownHeight - blockLag
|
|
1967
2240
|
if headBlockNumber <= 0 {
|
|
@@ -2079,8 +2352,6 @@ let getNextQuery = (
|
|
|
2079
2352
|
~candidates,
|
|
2080
2353
|
~headBlockNumber,
|
|
2081
2354
|
~chainTargetBlock,
|
|
2082
|
-
~chunkItemsMultiplier,
|
|
2083
|
-
~itemsTargetFloor,
|
|
2084
2355
|
~partitionBudget,
|
|
2085
2356
|
~queryEndBlock=computeQueryEndBlock(p),
|
|
2086
2357
|
) {
|
|
@@ -2124,8 +2395,6 @@ let getNextQuery = (
|
|
|
2124
2395
|
~inRangeCount,
|
|
2125
2396
|
~chainTargetBlock,
|
|
2126
2397
|
~freshBudget,
|
|
2127
|
-
~chunkItemsMultiplier,
|
|
2128
|
-
~itemsTargetFloor,
|
|
2129
2398
|
)
|
|
2130
2399
|
|
|
2131
2400
|
acceptCandidates(
|
|
@@ -2197,6 +2466,7 @@ let make = (
|
|
|
2197
2466
|
~onBlockRegistrations=[],
|
|
2198
2467
|
~blockLag=0,
|
|
2199
2468
|
~firstEventBlock=None,
|
|
2469
|
+
~clientFilterAddressThreshold=None,
|
|
2200
2470
|
): t => {
|
|
2201
2471
|
let latestFetchedBlock = {
|
|
2202
2472
|
blockTimestamp: 0,
|
|
@@ -2244,6 +2514,7 @@ let make = (
|
|
|
2244
2514
|
|
|
2245
2515
|
let registeringContractsByContract: dict<dict<indexingAddress>> = Dict.make()
|
|
2246
2516
|
let dynamicContracts = Utils.Set.make()
|
|
2517
|
+
let clientFilteredContracts = Utils.Set.make()
|
|
2247
2518
|
|
|
2248
2519
|
addresses->Array.forEach(contract => {
|
|
2249
2520
|
let contractName = contract.contractName
|
|
@@ -2265,9 +2536,29 @@ let make = (
|
|
|
2265
2536
|
}
|
|
2266
2537
|
})
|
|
2267
2538
|
|
|
2539
|
+
// Switch any contract already over the server-side address threshold to
|
|
2540
|
+
// client-side filtering at creation — a config contract with a large static
|
|
2541
|
+
// address list, or a dynamic contract restored from a large persisted set.
|
|
2542
|
+
switch clientFilterAddressThreshold {
|
|
2543
|
+
| Some(threshold) =>
|
|
2544
|
+
registeringContractsByContract->Utils.Dict.forEachWithKey((addrs, contractName) => {
|
|
2545
|
+
let addressCount = addrs->Utils.Dict.size
|
|
2546
|
+
if addressCount > threshold {
|
|
2547
|
+
clientFilteredContracts->addClientFilteredContract(
|
|
2548
|
+
~contractName,
|
|
2549
|
+
~chainId,
|
|
2550
|
+
~addressCount,
|
|
2551
|
+
~threshold,
|
|
2552
|
+
)
|
|
2553
|
+
}
|
|
2554
|
+
})
|
|
2555
|
+
| None => ()
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2268
2558
|
let optimizedPartitions = createPartitionsFromIndexingAddresses(
|
|
2269
2559
|
~registeringContractsByContract,
|
|
2270
2560
|
~dynamicContracts,
|
|
2561
|
+
~clientFilteredContracts,
|
|
2271
2562
|
~normalSelection,
|
|
2272
2563
|
~maxAddrInPartition,
|
|
2273
2564
|
~nextPartitionIndex=partitions->Array.length,
|
|
@@ -2325,6 +2616,7 @@ let make = (
|
|
|
2325
2616
|
knownHeight,
|
|
2326
2617
|
buffer,
|
|
2327
2618
|
firstEventBlock,
|
|
2619
|
+
clientFilterAddressThreshold,
|
|
2328
2620
|
}
|
|
2329
2621
|
|
|
2330
2622
|
fetchState
|
|
@@ -2450,6 +2742,7 @@ let rollback = (fetchState: t, ~indexingAddresses: IndexingAddresses.t, ~targetB
|
|
|
2450
2742
|
let optimizedPartitions = createPartitionsFromIndexingAddresses(
|
|
2451
2743
|
~registeringContractsByContract,
|
|
2452
2744
|
~dynamicContracts=fetchState.optimizedPartitions.dynamicContracts,
|
|
2745
|
+
~clientFilteredContracts=fetchState.optimizedPartitions.clientFilteredContracts,
|
|
2453
2746
|
~normalSelection=fetchState.normalSelection,
|
|
2454
2747
|
~maxAddrInPartition=fetchState.optimizedPartitions.maxAddrInPartition,
|
|
2455
2748
|
~nextPartitionIndex=nextKeptIdRef.contents,
|