envio 3.3.0-alpha.11 → 3.3.0-alpha.13
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/package.json +6 -6
- package/src/ChainFetching.res +21 -12
- package/src/ChainFetching.res.mjs +20 -9
- package/src/ChainState.res +20 -8
- package/src/ChainState.res.mjs +13 -12
- package/src/ChainState.resi +1 -1
- package/src/CrossChainState.res +64 -41
- package/src/CrossChainState.res.mjs +18 -13
- 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/FetchState.res +115 -76
- package/src/FetchState.res.mjs +114 -104
- package/src/InMemoryStore.res +14 -26
- package/src/InMemoryStore.res.mjs +6 -19
- package/src/IndexerState.res +10 -19
- package/src/IndexerState.res.mjs +7 -6
- package/src/IndexerState.resi +1 -9
- package/src/Internal.res +87 -12
- 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/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/Rollback.res +32 -13
- package/src/Rollback.res.mjs +24 -11
- package/src/UserContext.res +62 -23
- package/src/UserContext.res.mjs +26 -6
- package/src/Writing.res +28 -12
- package/src/Writing.res.mjs +15 -7
- package/src/bindings/NodeJs.res +5 -0
- package/src/sources/SvmHyperSyncSource.res +10 -0
- package/src/sources/SvmHyperSyncSource.res.mjs +1 -1
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Utils from "./Utils.res.mjs";
|
|
4
|
+
import * as Internal from "./Internal.res.mjs";
|
|
5
|
+
|
|
6
|
+
function make() {
|
|
7
|
+
return {
|
|
8
|
+
tables: {},
|
|
9
|
+
rateLimits: {}
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getRateLimitState(self, tableName, effect) {
|
|
14
|
+
let match = effect.rateLimit;
|
|
15
|
+
if (match === undefined) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
let callsPerDuration = match.callsPerDuration;
|
|
19
|
+
let existing = self.rateLimits[tableName];
|
|
20
|
+
let tmp;
|
|
21
|
+
if (existing !== undefined) {
|
|
22
|
+
tmp = existing;
|
|
23
|
+
} else {
|
|
24
|
+
let created = {
|
|
25
|
+
callsPerDuration: callsPerDuration,
|
|
26
|
+
durationMs: match.durationMs,
|
|
27
|
+
availableCalls: callsPerDuration,
|
|
28
|
+
windowStartTime: Date.now(),
|
|
29
|
+
queueCount: 0,
|
|
30
|
+
nextWindowPromise: undefined
|
|
31
|
+
};
|
|
32
|
+
self.rateLimits[tableName] = created;
|
|
33
|
+
tmp = created;
|
|
34
|
+
}
|
|
35
|
+
return tmp;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getTable(self, effect, scope) {
|
|
39
|
+
let tableName = Internal.EffectCache.toTableName(effect.name, scope);
|
|
40
|
+
let inMemTable = self.tables[tableName];
|
|
41
|
+
if (inMemTable !== undefined) {
|
|
42
|
+
return inMemTable;
|
|
43
|
+
}
|
|
44
|
+
let inMemTable$1 = {
|
|
45
|
+
idsToStore: [],
|
|
46
|
+
invalidationsCount: 0,
|
|
47
|
+
dict: {},
|
|
48
|
+
changesCount: 0,
|
|
49
|
+
effect: effect,
|
|
50
|
+
scope: scope,
|
|
51
|
+
table: Internal.makeCacheTable(effect.name, scope),
|
|
52
|
+
rateLimitState: getRateLimitState(self, tableName, effect),
|
|
53
|
+
activeCallsCount: 0,
|
|
54
|
+
prevCallStartTimerRef: null
|
|
55
|
+
};
|
|
56
|
+
self.tables[tableName] = inMemTable$1;
|
|
57
|
+
return inMemTable$1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function forEach(self, fn) {
|
|
61
|
+
Utils.Dict.forEach(self.tables, fn);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function resetForRollback(self) {
|
|
65
|
+
self.tables = {};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export {
|
|
69
|
+
make,
|
|
70
|
+
getTable,
|
|
71
|
+
forEach,
|
|
72
|
+
resetForRollback,
|
|
73
|
+
}
|
|
74
|
+
/* Utils Not a pure module */
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
type effectRateLimitState = {
|
|
2
|
+
callsPerDuration: int,
|
|
3
|
+
durationMs: int,
|
|
4
|
+
mutable availableCalls: int,
|
|
5
|
+
mutable windowStartTime: float,
|
|
6
|
+
mutable queueCount: int,
|
|
7
|
+
mutable nextWindowPromise: option<promise<unit>>,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type effectCacheInMemTable = {
|
|
11
|
+
mutable idsToStore: array<string>,
|
|
12
|
+
mutable invalidationsCount: int,
|
|
13
|
+
mutable dict: dict<Change.t<Internal.effectOutput>>,
|
|
14
|
+
mutable changesCount: float,
|
|
15
|
+
effect: Internal.effect,
|
|
16
|
+
scope: Internal.chainScope,
|
|
17
|
+
table: Table.table,
|
|
18
|
+
rateLimitState: option<effectRateLimitState>,
|
|
19
|
+
mutable activeCallsCount: int,
|
|
20
|
+
mutable prevCallStartTimerRef: Performance.timeRef,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Opaque store of all per-(effect, scope) runtime state.
|
|
24
|
+
type t
|
|
25
|
+
|
|
26
|
+
let make: unit => t
|
|
27
|
+
// Get, or lazily create, the in-mem table for an (effect, scope). Reuses the
|
|
28
|
+
// surviving rate-limit window when the table is recreated after a rollback.
|
|
29
|
+
let getTable: (t, ~effect: Internal.effect, ~scope: Internal.chainScope) => effectCacheInMemTable
|
|
30
|
+
let forEach: (t, effectCacheInMemTable => unit) => unit
|
|
31
|
+
// Drop the cache tables but keep the rate-limit windows across a rollback.
|
|
32
|
+
let resetForRollback: t => unit
|
package/src/Envio.res
CHANGED
|
@@ -186,11 +186,19 @@ and effectOptions<'input, 'output> = {
|
|
|
186
186
|
rateLimit: rateLimit,
|
|
187
187
|
/** Whether the effect should be cached. */
|
|
188
188
|
cache?: bool,
|
|
189
|
+
/** Whether the effect's cache is shared across all chains. Defaults to `true`.
|
|
190
|
+
Set to `false` to isolate the cache (and rate limiting) per chain and enable
|
|
191
|
+
`context.chain.id` inside the handler. */
|
|
192
|
+
crossChain?: bool,
|
|
189
193
|
}
|
|
194
|
+
and effectChain = {id: int}
|
|
190
195
|
and effectContext = {
|
|
191
196
|
log: logger,
|
|
192
197
|
effect: 'input 'output. (effect<'input, 'output>, 'input) => promise<'output>,
|
|
193
198
|
mutable cache: bool,
|
|
199
|
+
/** The chain the effect was called on. Only available on effects created with
|
|
200
|
+
`crossChain: false`; accessing it on a cross-chain effect throws. */
|
|
201
|
+
chain: effectChain,
|
|
194
202
|
}
|
|
195
203
|
and effectArgs<'input> = {
|
|
196
204
|
input: 'input,
|
|
@@ -205,10 +213,23 @@ let durationToMs = (duration: rateLimitDuration) =>
|
|
|
205
213
|
| Milliseconds(ms) => ms
|
|
206
214
|
}
|
|
207
215
|
|
|
216
|
+
// The name becomes both a Postgres cache-table suffix and a .envio/cache file
|
|
217
|
+
// path segment. Path separators are excluded from the charset and a leading dot
|
|
218
|
+
// is disallowed, so a name can never be "." / ".." or otherwise traverse out of
|
|
219
|
+
// the cache dir; dots elsewhere are fine and keep existing names like
|
|
220
|
+
// "token.metadata" working, since the (name, scope) <-> table <-> path mapping
|
|
221
|
+
// stays reversible.
|
|
222
|
+
let effectNameRe = /^[A-Za-z0-9_-][A-Za-z0-9_.-]*$/
|
|
223
|
+
|
|
208
224
|
let createEffect = (
|
|
209
225
|
options: effectOptions<'input, 'output>,
|
|
210
226
|
handler: effectArgs<'input> => promise<'output>,
|
|
211
227
|
) => {
|
|
228
|
+
if !(effectNameRe->RegExp.test(options.name)) {
|
|
229
|
+
JsError.throwWithMessage(
|
|
230
|
+
`Invalid effect name "${options.name}". Effect names may contain letters, numbers, underscores, hyphens and dots (but must not start with a dot or contain a path separator), because the name is used as the cache table name and cache file path.`,
|
|
231
|
+
)
|
|
232
|
+
}
|
|
212
233
|
let outputSchema =
|
|
213
234
|
S.schema(_ => options.output)->(Utils.magic: S.t<S.t<'output>> => S.t<Internal.effectOutput>)
|
|
214
235
|
let itemSchema = S.schema((s): Internal.effectCacheItem => {
|
|
@@ -222,8 +243,6 @@ let createEffect = (
|
|
|
222
243
|
Internal.effectOutput,
|
|
223
244
|
>
|
|
224
245
|
),
|
|
225
|
-
activeCallsCount: 0,
|
|
226
|
-
prevCallStartTimerRef: %raw(`null`),
|
|
227
246
|
// This is the way to make the createEffect API
|
|
228
247
|
// work without the need for users to call S.schema themselves,
|
|
229
248
|
// but simply pass the desired object/tuple/etc.
|
|
@@ -233,7 +252,6 @@ let createEffect = (
|
|
|
233
252
|
),
|
|
234
253
|
output: outputSchema,
|
|
235
254
|
storageMeta: {
|
|
236
|
-
table: Internal.makeCacheTable(~effectName=options.name),
|
|
237
255
|
outputSchema,
|
|
238
256
|
itemSchema,
|
|
239
257
|
},
|
|
@@ -241,16 +259,16 @@ let createEffect = (
|
|
|
241
259
|
| Some(true) => true
|
|
242
260
|
| _ => false
|
|
243
261
|
},
|
|
262
|
+
crossChain: switch options.crossChain {
|
|
263
|
+
| Some(false) => false
|
|
264
|
+
| _ => true
|
|
265
|
+
},
|
|
244
266
|
rateLimit: switch options.rateLimit {
|
|
245
267
|
| Disable => None
|
|
246
268
|
| Enable({calls, per}) =>
|
|
247
269
|
Some({
|
|
248
270
|
callsPerDuration: calls,
|
|
249
271
|
durationMs: per->durationToMs,
|
|
250
|
-
availableCalls: calls,
|
|
251
|
-
windowStartTime: Date.now(),
|
|
252
|
-
queueCount: 0,
|
|
253
|
-
nextWindowPromise: None,
|
|
254
272
|
})
|
|
255
273
|
},
|
|
256
274
|
}->(Utils.magic: Internal.effect => effect<'input, 'output>)
|
package/src/Envio.res.mjs
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import * as Address from "./Address.res.mjs";
|
|
4
4
|
import * as Process from "process";
|
|
5
|
-
import * as Internal from "./Internal.res.mjs";
|
|
6
5
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
|
+
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
7
7
|
import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs";
|
|
8
8
|
|
|
9
9
|
function durationToMs(duration) {
|
|
@@ -18,41 +18,36 @@ function durationToMs(duration) {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
let effectNameRe = /^[A-Za-z0-9_-][A-Za-z0-9_.-]*$/;
|
|
22
|
+
|
|
21
23
|
function createEffect(options, handler) {
|
|
24
|
+
if (!effectNameRe.test(options.name)) {
|
|
25
|
+
Stdlib_JsError.throwWithMessage(`Invalid effect name "` + options.name + `". Effect names may contain letters, numbers, underscores, hyphens and dots (but must not start with a dot or contain a path separator), because the name is used as the cache table name and cache file path.`);
|
|
26
|
+
}
|
|
22
27
|
let outputSchema = S$RescriptSchema.schema(param => options.output);
|
|
23
28
|
let itemSchema = S$RescriptSchema.schema(s => ({
|
|
24
29
|
id: s.m(S$RescriptSchema.string),
|
|
25
30
|
output: s.m(outputSchema)
|
|
26
31
|
}));
|
|
27
32
|
let match = options.cache;
|
|
28
|
-
let match$1 = options.
|
|
33
|
+
let match$1 = options.crossChain;
|
|
34
|
+
let match$2 = options.rateLimit;
|
|
29
35
|
let tmp;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
tmp = {
|
|
35
|
-
callsPerDuration: calls,
|
|
36
|
-
durationMs: durationToMs(match$1.per),
|
|
37
|
-
availableCalls: calls,
|
|
38
|
-
windowStartTime: Date.now(),
|
|
39
|
-
queueCount: 0,
|
|
40
|
-
nextWindowPromise: undefined
|
|
41
|
-
};
|
|
42
|
-
}
|
|
36
|
+
tmp = match$2 === false ? undefined : ({
|
|
37
|
+
callsPerDuration: match$2.calls,
|
|
38
|
+
durationMs: durationToMs(match$2.per)
|
|
39
|
+
});
|
|
43
40
|
return {
|
|
44
41
|
name: options.name,
|
|
45
42
|
handler: handler,
|
|
46
43
|
storageMeta: {
|
|
47
44
|
itemSchema: itemSchema,
|
|
48
|
-
outputSchema: outputSchema
|
|
49
|
-
table: Internal.makeCacheTable(options.name)
|
|
45
|
+
outputSchema: outputSchema
|
|
50
46
|
},
|
|
51
47
|
defaultShouldCache: match !== undefined ? match : false,
|
|
48
|
+
crossChain: match$1 !== undefined ? match$1 : true,
|
|
52
49
|
output: outputSchema,
|
|
53
50
|
input: S$RescriptSchema.schema(param => options.input),
|
|
54
|
-
activeCallsCount: 0,
|
|
55
|
-
prevCallStartTimerRef: null,
|
|
56
51
|
rateLimit: tmp
|
|
57
52
|
};
|
|
58
53
|
}
|
|
@@ -102,6 +97,7 @@ let TestHelpers = {
|
|
|
102
97
|
|
|
103
98
|
export {
|
|
104
99
|
durationToMs,
|
|
100
|
+
effectNameRe,
|
|
105
101
|
createEffect,
|
|
106
102
|
isNonInteractive,
|
|
107
103
|
TestHelpers,
|
package/src/FetchState.res
CHANGED
|
@@ -47,17 +47,19 @@ type partition = {
|
|
|
47
47
|
dynamicContract: option<string>,
|
|
48
48
|
// Mutable array for SourceManager sync - queries exist only while being fetched
|
|
49
49
|
mutPendingQueries: array<pendingQuery>,
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
|
|
50
|
+
// The last two ranges that measured how many blocks the source could return.
|
|
51
|
+
// Both must be non-zero before the minimum is trusted for chunk sizing.
|
|
52
|
+
sourceRangeCapacity: int,
|
|
53
|
+
prevSourceRangeCapacity: int,
|
|
54
|
+
// Smoothed items/block observed in responses. This is independent from the
|
|
55
|
+
// source's range capacity: even a response truncated by our own itemsTarget
|
|
56
|
+
// cap is useful density evidence while saying nothing about source capacity.
|
|
57
|
+
// None distinguishes a new partition from a real zero-density observation.
|
|
58
|
+
eventDensity: option<float>,
|
|
57
59
|
// Tracks the latestFetchedBlock.blockNumber of the most recent response
|
|
58
|
-
// that updated
|
|
59
|
-
// when parallel query responses arrive out of order.
|
|
60
|
-
|
|
60
|
+
// that updated sourceRangeCapacity. Prevents degradation of the chunking
|
|
61
|
+
// heuristic when parallel query responses arrive out of order.
|
|
62
|
+
latestSourceRangeCapacityUpdateBlock: int,
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
type query = {
|
|
@@ -111,22 +113,17 @@ let densityItemsTarget = (~density, ~fromBlock, ~toBlock, ~chainTargetBlock) =>
|
|
|
111
113
|
)
|
|
112
114
|
}
|
|
113
115
|
|
|
114
|
-
// Calculate the chunk range from
|
|
116
|
+
// Calculate the chunk range from the last two source-capacity observations.
|
|
115
117
|
let getMinHistoryRange = (p: partition) => {
|
|
116
|
-
switch (p.
|
|
118
|
+
switch (p.sourceRangeCapacity, p.prevSourceRangeCapacity) {
|
|
117
119
|
| (0, _) | (_, 0) => None
|
|
118
120
|
| (a, b) => Some(a < b ? a : b)
|
|
119
121
|
}
|
|
120
122
|
}
|
|
121
123
|
|
|
122
|
-
// Density
|
|
123
|
-
//
|
|
124
|
-
let getTrustedDensity = (p: partition) =>
|
|
125
|
-
switch (p.prevQueryRange, p.prevPrevQueryRange) {
|
|
126
|
-
| (0, _) | (_, 0) => None
|
|
127
|
-
| (prevQueryRange, _) => Some(p.prevRangeSize->Int.toFloat /. prevQueryRange->Int.toFloat)
|
|
128
|
-
}
|
|
129
|
-
}
|
|
124
|
+
// Density has its own initialization signal and is useful independently from
|
|
125
|
+
// source-capacity history, including when an itemsTarget cap truncates a response.
|
|
126
|
+
let getTrustedDensity = (p: partition) => p.eventDensity
|
|
130
127
|
|
|
131
128
|
let getMinQueryRange = (partitions: array<partition>) => {
|
|
132
129
|
let min = ref(0)
|
|
@@ -134,8 +131,8 @@ let getMinQueryRange = (partitions: array<partition>) => {
|
|
|
134
131
|
let p = partitions->Array.getUnsafe(i)
|
|
135
132
|
|
|
136
133
|
// Even if it's fetching, set dynamicContract field
|
|
137
|
-
let a = p.
|
|
138
|
-
let b = p.
|
|
134
|
+
let a = p.sourceRangeCapacity
|
|
135
|
+
let b = p.prevSourceRangeCapacity
|
|
139
136
|
if a > 0 && (min.contents == 0 || a < min.contents) {
|
|
140
137
|
min := a
|
|
141
138
|
}
|
|
@@ -208,11 +205,14 @@ module OptimizedPartitions = {
|
|
|
208
205
|
nextPartitionIndexRef := nextPartitionIndexRef.contents + 1
|
|
209
206
|
let minRange = getMinQueryRange([p1, p2])
|
|
210
207
|
// The merged partition indexes both parents' addresses, so its expected
|
|
211
|
-
// event rate is the sum of their densities.
|
|
212
|
-
// density
|
|
213
|
-
//
|
|
214
|
-
let inheritedDensity =
|
|
215
|
-
|
|
208
|
+
// event rate is the sum of their densities. If neither parent has a
|
|
209
|
+
// trusted density, the merged partition probes for a fresh signal
|
|
210
|
+
// instead of treating zero as an observed density.
|
|
211
|
+
let inheritedDensity = switch (p1->getTrustedDensity, p2->getTrustedDensity) {
|
|
212
|
+
| (Some(p1Density), Some(p2Density)) => Some(p1Density +. p2Density)
|
|
213
|
+
| (Some(density), None) | (None, Some(density)) => Some(density)
|
|
214
|
+
| (None, None) => None
|
|
215
|
+
}
|
|
216
216
|
{
|
|
217
217
|
id: newId,
|
|
218
218
|
dynamicContract: Some(contractName),
|
|
@@ -221,10 +221,10 @@ module OptimizedPartitions = {
|
|
|
221
221
|
mergeBlock: None,
|
|
222
222
|
addressesByContractName: Dict.make(), // set below
|
|
223
223
|
mutPendingQueries: [],
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
224
|
+
sourceRangeCapacity: minRange,
|
|
225
|
+
prevSourceRangeCapacity: minRange,
|
|
226
|
+
eventDensity: inheritedDensity,
|
|
227
|
+
latestSourceRangeCapacityUpdateBlock: 0,
|
|
228
228
|
}
|
|
229
229
|
}
|
|
230
230
|
|
|
@@ -472,14 +472,28 @@ module OptimizedPartitions = {
|
|
|
472
472
|
pendingQuery.fetchedBlock = Some(latestFetchedBlock)
|
|
473
473
|
|
|
474
474
|
let blockRange = latestFetchedBlock.blockNumber - query.fromBlock + 1
|
|
475
|
-
//
|
|
475
|
+
// Update density for every response, independently from whether this range
|
|
476
|
+
// is valid evidence of source capacity. A cap hit is still useful density
|
|
477
|
+
// evidence because it reports items returned across the scanned range.
|
|
478
|
+
let observedEventDensity = itemsCount->Int.toFloat /. blockRange->Int.toFloat
|
|
479
|
+
// Seed from the first observation, then smooth every later observation
|
|
480
|
+
// with a 1:1 moving average. Keeping initialization explicit is important:
|
|
481
|
+
// zero is valid density evidence and must participate in the next blend.
|
|
482
|
+
let updatedEventDensity = switch p.eventDensity {
|
|
483
|
+
| None => Some(observedEventDensity)
|
|
484
|
+
| Some(eventDensity) => Some((eventDensity +. observedEventDensity) /. 2.)
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Skip updating source capacity if a later response already updated it.
|
|
476
488
|
// Prevents degradation of the chunking heuristic when parallel query
|
|
477
489
|
// responses arrive out of order (e.g. earlier query with smaller range
|
|
478
490
|
// arriving after a later query with bigger range).
|
|
479
|
-
let
|
|
480
|
-
latestFetchedBlock.blockNumber > p.
|
|
491
|
+
let shouldUpdateSourceRangeCapacity =
|
|
492
|
+
latestFetchedBlock.blockNumber > p.latestSourceRangeCapacityUpdateBlock &&
|
|
481
493
|
switch query.toBlock {
|
|
482
|
-
| None =>
|
|
494
|
+
| None =>
|
|
495
|
+
// Don't update source capacity when very close to the head.
|
|
496
|
+
latestFetchedBlock.blockNumber < knownHeight - 10
|
|
483
497
|
| Some(queryToBlock) =>
|
|
484
498
|
if latestFetchedBlock.blockNumber < queryToBlock {
|
|
485
499
|
// Partial response is direct capacity evidence — unless it was
|
|
@@ -497,9 +511,12 @@ module OptimizedPartitions = {
|
|
|
497
511
|
}
|
|
498
512
|
}
|
|
499
513
|
}
|
|
500
|
-
let
|
|
501
|
-
|
|
502
|
-
|
|
514
|
+
let updatedSourceRangeCapacity = shouldUpdateSourceRangeCapacity
|
|
515
|
+
? blockRange
|
|
516
|
+
: p.sourceRangeCapacity
|
|
517
|
+
let updatedPrevSourceRangeCapacity = shouldUpdateSourceRangeCapacity
|
|
518
|
+
? p.sourceRangeCapacity
|
|
519
|
+
: p.prevSourceRangeCapacity
|
|
503
520
|
|
|
504
521
|
// Process fetched queries from front of queue for main partition
|
|
505
522
|
let updatedLatestFetchedBlock = consumeFetchedQueries(
|
|
@@ -519,12 +536,12 @@ module OptimizedPartitions = {
|
|
|
519
536
|
let updatedMainPartition = {
|
|
520
537
|
...p,
|
|
521
538
|
latestFetchedBlock: updatedLatestFetchedBlock,
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
539
|
+
sourceRangeCapacity: updatedSourceRangeCapacity,
|
|
540
|
+
prevSourceRangeCapacity: updatedPrevSourceRangeCapacity,
|
|
541
|
+
eventDensity: updatedEventDensity,
|
|
542
|
+
latestSourceRangeCapacityUpdateBlock: shouldUpdateSourceRangeCapacity
|
|
526
543
|
? latestFetchedBlock.blockNumber
|
|
527
|
-
: p.
|
|
544
|
+
: p.latestSourceRangeCapacityUpdateBlock,
|
|
528
545
|
}
|
|
529
546
|
|
|
530
547
|
mutEntities->Dict.set(p.id, updatedMainPartition)
|
|
@@ -986,10 +1003,10 @@ OptimizedPartitions.t => {
|
|
|
986
1003
|
addressesByContractName,
|
|
987
1004
|
mergeBlock: None,
|
|
988
1005
|
mutPendingQueries: [],
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1006
|
+
sourceRangeCapacity: 0,
|
|
1007
|
+
prevSourceRangeCapacity: 0,
|
|
1008
|
+
eventDensity: None,
|
|
1009
|
+
latestSourceRangeCapacityUpdateBlock: 0,
|
|
993
1010
|
})
|
|
994
1011
|
nextPartitionIndexRef := nextPartitionIndexRef.contents + 1
|
|
995
1012
|
}
|
|
@@ -1322,10 +1339,10 @@ let registerDynamicContracts = (
|
|
|
1322
1339
|
addressesByContractName,
|
|
1323
1340
|
mergeBlock: None,
|
|
1324
1341
|
mutPendingQueries: p.mutPendingQueries,
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1342
|
+
sourceRangeCapacity: p.sourceRangeCapacity,
|
|
1343
|
+
prevSourceRangeCapacity: p.prevSourceRangeCapacity,
|
|
1344
|
+
eventDensity: p.eventDensity,
|
|
1345
|
+
latestSourceRangeCapacityUpdateBlock: p.latestSourceRangeCapacityUpdateBlock,
|
|
1329
1346
|
})
|
|
1330
1347
|
}
|
|
1331
1348
|
}
|
|
@@ -1448,6 +1465,11 @@ let startFetchingQueries = ({optimizedPartitions}: t, ~queries: array<query>) =>
|
|
|
1448
1465
|
// Most parallel in-flight chunk queries a single partition may have at once.
|
|
1449
1466
|
let maxPendingChunksPerPartition = 12
|
|
1450
1467
|
|
|
1468
|
+
// Most parallel in-flight queries a single chain may have at once, across all
|
|
1469
|
+
// its partitions. Bounds source load on chains with many partitions, where the
|
|
1470
|
+
// per-partition cap alone would admit thousands of concurrent queries.
|
|
1471
|
+
let maxChainConcurrency = 100
|
|
1472
|
+
|
|
1451
1473
|
// Generates candidate queries for a gap range (a hole left between
|
|
1452
1474
|
// completed/pending chunks, e.g. from an out-of-order partial response). Gaps
|
|
1453
1475
|
// carry the range's low fromBlock, so the acceptance pass takes them before
|
|
@@ -1582,24 +1604,25 @@ type partitionFillState = {
|
|
|
1582
1604
|
// query with no other ceiling: the true hard bounds stay
|
|
1583
1605
|
// endBlock/mergeBlock/the lagged head.
|
|
1584
1606
|
//
|
|
1585
|
-
// The tick's budget is chainTargetItems minus what's already in flight.
|
|
1586
|
-
//
|
|
1587
|
-
//
|
|
1588
|
-
//
|
|
1589
|
-
//
|
|
1590
|
-
//
|
|
1607
|
+
// The tick's budget is chainTargetItems minus what's already in flight. A
|
|
1608
|
+
// non-positive budget only resolves the wait action and generates no query
|
|
1609
|
+
// candidates. With a positive budget, every candidate query — gap-fill holes,
|
|
1610
|
+
// plus each in-range partition's chunks or open-ended probe toward the target —
|
|
1611
|
+
// is generated with no budget check, then the candidates are sorted by
|
|
1612
|
+
// fromBlock and accepted in that order while the budget stays positive. The
|
|
1613
|
+
// query that tips it negative is still accepted (a single overshoot);
|
|
1614
|
+
// everything after it waits for a tick with more budget.
|
|
1591
1615
|
// Sorting by fromBlock spends the budget on the earliest blocks across all
|
|
1592
1616
|
// partitions first, so no partition is starved by generation order and the
|
|
1593
1617
|
// frontier advances evenly. In-flight reservations release as responses land,
|
|
1594
1618
|
// so acceptance redistributes across ticks.
|
|
1595
1619
|
//
|
|
1596
|
-
// A partition with a
|
|
1597
|
-
//
|
|
1598
|
-
//
|
|
1599
|
-
//
|
|
1600
|
-
//
|
|
1601
|
-
//
|
|
1602
|
-
// one budget, each scaled by how much of the range it still has to cover.
|
|
1620
|
+
// A partition with source-capacity history and a positive density generates
|
|
1621
|
+
// density-sized chunks toward the target. Any other partition (no signal, no
|
|
1622
|
+
// capacity history, or a density-0 estimate) generates one open-ended probe
|
|
1623
|
+
// sized to the events its range to the target is expected to hold —
|
|
1624
|
+
// rangeTargetDensity × (chainTargetBlock − fromBlock + 1) / inRangeCount — so
|
|
1625
|
+
// unknown-density partitions probe in parallel within one budget.
|
|
1603
1626
|
let getNextQuery = (
|
|
1604
1627
|
{optimizedPartitions, blockLag, latestOnBlockBlockNumber, knownHeight, endBlock}: t,
|
|
1605
1628
|
~chainTargetBlock: int,
|
|
@@ -1639,6 +1662,12 @@ let getNextQuery = (
|
|
|
1639
1662
|
}
|
|
1640
1663
|
}
|
|
1641
1664
|
|
|
1665
|
+
// A zero budget is an admission check: preserve the wait-state scan above,
|
|
1666
|
+
// but make every query-generation pass below empty. Caught-up chains also
|
|
1667
|
+
// skip those passes because their action is already known.
|
|
1668
|
+
let partitionsCount =
|
|
1669
|
+
chainTargetItems <= 0. || shouldWaitForNewBlock.contents ? 0 : partitionsCount
|
|
1670
|
+
|
|
1642
1671
|
// One bucket per partition, in idsInAscOrder order — gap-fill and the
|
|
1643
1672
|
// budget pass both push into a partition's own bucket, so flattening at
|
|
1644
1673
|
// the end (see below) reproduces idsInAscOrder without a sort.
|
|
@@ -1788,10 +1817,9 @@ let getNextQuery = (
|
|
|
1788
1817
|
inRangeCount > 0 && rangeToTarget > 0 ? freshBudget /. rangeToTarget->Int.toFloat : 0.
|
|
1789
1818
|
|
|
1790
1819
|
// Phase B: generate each in-range partition's candidates — strict chunks
|
|
1791
|
-
//
|
|
1792
|
-
//
|
|
1793
|
-
//
|
|
1794
|
-
// acceptance pass below decides which candidates make the cut.
|
|
1820
|
+
// when both source-capacity history and density are known, or an open-ended
|
|
1821
|
+
// budget probe otherwise. No budget check here; the acceptance pass below
|
|
1822
|
+
// decides which candidates make the cut.
|
|
1795
1823
|
//
|
|
1796
1824
|
// Chunks require a POSITIVE trusted density: density 0 prices every chunk at
|
|
1797
1825
|
// ~nothing, so an open-ended probe (full server scan range in one response)
|
|
@@ -1916,12 +1944,21 @@ let getNextQuery = (
|
|
|
1916
1944
|
let streamCount = acceptanceStream->Array.length
|
|
1917
1945
|
let remainingBudget = ref(chainTargetItems)
|
|
1918
1946
|
let acceptIdx = ref(0)
|
|
1919
|
-
|
|
1947
|
+
// In-flight queries count against the chain's concurrency cap alongside the
|
|
1948
|
+
// ones accepted this tick; once the cap is reached no later candidate can be
|
|
1949
|
+
// accepted (they're only later in fromBlock order), so the walk stops.
|
|
1950
|
+
let usedConcurrency = ref(reservations->Array.length)
|
|
1951
|
+
while (
|
|
1952
|
+
remainingBudget.contents > 0. &&
|
|
1953
|
+
acceptIdx.contents < streamCount &&
|
|
1954
|
+
usedConcurrency.contents < maxChainConcurrency
|
|
1955
|
+
) {
|
|
1920
1956
|
let (_, itemsEst, maybeQuery) = acceptanceStream->Array.getUnsafe(acceptIdx.contents)
|
|
1921
1957
|
switch maybeQuery {
|
|
1922
1958
|
| Some(query) =>
|
|
1923
1959
|
let partitionIdx = partitionIndexById->Dict.getUnsafe(query.partitionId)
|
|
1924
1960
|
queriesByPartitionIndex->Array.getUnsafe(partitionIdx)->Array.push(query)->ignore
|
|
1961
|
+
usedConcurrency := usedConcurrency.contents + 1
|
|
1925
1962
|
| None => ()
|
|
1926
1963
|
}
|
|
1927
1964
|
remainingBudget := remainingBudget.contents -. itemsEst->Int.toFloat
|
|
@@ -2022,10 +2059,10 @@ let make = (
|
|
|
2022
2059
|
mergeBlock: None,
|
|
2023
2060
|
dynamicContract: None,
|
|
2024
2061
|
mutPendingQueries: [],
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2062
|
+
sourceRangeCapacity: 0,
|
|
2063
|
+
prevSourceRangeCapacity: 0,
|
|
2064
|
+
eventDensity: None,
|
|
2065
|
+
latestSourceRangeCapacityUpdateBlock: 0,
|
|
2029
2066
|
})
|
|
2030
2067
|
}
|
|
2031
2068
|
|
|
@@ -2345,13 +2382,15 @@ let isReadyToEnterReorgThreshold = ({endBlock, blockLag, buffer, knownHeight} as
|
|
|
2345
2382
|
buffer->Utils.Array.isEmpty
|
|
2346
2383
|
}
|
|
2347
2384
|
|
|
2348
|
-
// Lower progress percentage = further behind = higher priority.
|
|
2349
|
-
//
|
|
2385
|
+
// Lower progress percentage = further behind = higher priority. Progress is
|
|
2386
|
+
// relative to the head this chain can actually fetch, so a chain at its lagged
|
|
2387
|
+
// head does not look behind relative to unavailable blocks. Shared by the batch
|
|
2388
|
+
// ordering and the cross-chain fetch priority.
|
|
2350
2389
|
let getProgressPercentage = (fetchState: t) => {
|
|
2351
2390
|
switch fetchState.firstEventBlock {
|
|
2352
2391
|
| None => 0.
|
|
2353
2392
|
| Some(firstEventBlock) =>
|
|
2354
|
-
let totalRange = fetchState.knownHeight - firstEventBlock
|
|
2393
|
+
let totalRange = fetchState.knownHeight - fetchState.blockLag - firstEventBlock
|
|
2355
2394
|
if totalRange <= 0 {
|
|
2356
2395
|
0.
|
|
2357
2396
|
} else {
|