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
package/index.d.ts
CHANGED
|
@@ -48,6 +48,13 @@ export type EffectCaller = <I, O>(
|
|
|
48
48
|
input: I extends undefined ? undefined : I
|
|
49
49
|
) => Promise<O>;
|
|
50
50
|
|
|
51
|
+
/** The chain an Effect was called on. Available only on chain-scoped effects
|
|
52
|
+
* (`crossChain: false`). */
|
|
53
|
+
export type EffectChain = {
|
|
54
|
+
/** The chain id the effect handler was called on. */
|
|
55
|
+
readonly id: number;
|
|
56
|
+
};
|
|
57
|
+
|
|
51
58
|
/** Context passed to an Effect's handler function. */
|
|
52
59
|
export type EffectContext = {
|
|
53
60
|
/** Access the logger instance with the event as context. */
|
|
@@ -57,6 +64,9 @@ export type EffectContext = {
|
|
|
57
64
|
/** Whether to cache this call's result. Defaults to the effect's `cache`
|
|
58
65
|
* option; set to `false` to skip caching for this specific invocation. */
|
|
59
66
|
cache: boolean;
|
|
67
|
+
/** The chain the effect was called on. Only available on effects created with
|
|
68
|
+
* `crossChain: false`; accessing it on a cross-chain effect throws. */
|
|
69
|
+
readonly chain: EffectChain;
|
|
60
70
|
};
|
|
61
71
|
|
|
62
72
|
/** Rate-limit window for an {@link Effect}. Strings resolve to common
|
|
@@ -82,6 +92,11 @@ export type EffectOptions<Input, Output> = {
|
|
|
82
92
|
readonly rateLimit: RateLimit;
|
|
83
93
|
/** Whether the effect should be cached. */
|
|
84
94
|
readonly cache?: boolean;
|
|
95
|
+
/** Whether the effect's cache is shared across all chains. Defaults to
|
|
96
|
+
* `true`. Set to `false` to isolate the cache and rate limiting per chain and
|
|
97
|
+
* enable `context.chain.id` inside the handler. Changing this changes the
|
|
98
|
+
* effect's cache identity. */
|
|
99
|
+
readonly crossChain?: boolean;
|
|
85
100
|
};
|
|
86
101
|
|
|
87
102
|
/** Arguments passed to the handler function of an {@link Effect}. */
|
|
@@ -209,6 +224,10 @@ export function createEffect<
|
|
|
209
224
|
readonly rateLimit: RateLimit;
|
|
210
225
|
/** Whether the effect should be cached. */
|
|
211
226
|
readonly cache?: boolean;
|
|
227
|
+
/** Whether the effect's cache is shared across all chains. Defaults to
|
|
228
|
+
* `true`. Set to `false` to isolate the cache and rate limiting per chain
|
|
229
|
+
* and enable `context.chain.id` inside the handler. */
|
|
230
|
+
readonly crossChain?: boolean;
|
|
212
231
|
},
|
|
213
232
|
handler: (args: EffectArgs<I>) => Promise<R>
|
|
214
233
|
): Effect<I, O>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "envio",
|
|
3
|
-
"version": "3.3.0-alpha.
|
|
3
|
+
"version": "3.3.0-alpha.13",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A latency and sync speed optimized, developer friendly blockchain data indexer.",
|
|
6
6
|
"bin": "./bin.mjs",
|
|
@@ -70,10 +70,10 @@
|
|
|
70
70
|
"tsx": "4.21.0"
|
|
71
71
|
},
|
|
72
72
|
"optionalDependencies": {
|
|
73
|
-
"envio-linux-x64": "3.3.0-alpha.
|
|
74
|
-
"envio-linux-x64-musl": "3.3.0-alpha.
|
|
75
|
-
"envio-linux-arm64": "3.3.0-alpha.
|
|
76
|
-
"envio-darwin-x64": "3.3.0-alpha.
|
|
77
|
-
"envio-darwin-arm64": "3.3.0-alpha.
|
|
73
|
+
"envio-linux-x64": "3.3.0-alpha.13",
|
|
74
|
+
"envio-linux-x64-musl": "3.3.0-alpha.13",
|
|
75
|
+
"envio-linux-arm64": "3.3.0-alpha.13",
|
|
76
|
+
"envio-darwin-x64": "3.3.0-alpha.13",
|
|
77
|
+
"envio-darwin-arm64": "3.3.0-alpha.13"
|
|
78
78
|
}
|
|
79
79
|
}
|
package/src/ChainFetching.res
CHANGED
|
@@ -149,19 +149,28 @@ let rec onQueryResponse = async (
|
|
|
149
149
|
|
|
150
150
|
let numContractRegisterEvents = parsedQueueItems->Array.reduce(0, (count, item) => {
|
|
151
151
|
let eventItem = item->Internal.castUnsafeEventItem
|
|
152
|
-
eventItem.onEventRegistration.contractRegister !== None
|
|
153
|
-
? count + 1
|
|
154
|
-
: count
|
|
155
|
-
})
|
|
156
|
-
Logging.trace({
|
|
157
|
-
"msg": "Finished querying",
|
|
158
|
-
"chainId": chain->ChainMap.Chain.toChainId,
|
|
159
|
-
"partitionId": query.partitionId,
|
|
160
|
-
"fromBlock": fromBlockQueried,
|
|
161
|
-
"toBlock": latestFetchedBlockNumber,
|
|
162
|
-
"numEvents": parsedQueueItems->Array.length,
|
|
163
|
-
"numContractRegisterEvents": numContractRegisterEvents,
|
|
152
|
+
eventItem.onEventRegistration.contractRegister !== None ? count + 1 : count
|
|
164
153
|
})
|
|
154
|
+
if numContractRegisterEvents === 0 {
|
|
155
|
+
Logging.trace({
|
|
156
|
+
"msg": "Finished querying",
|
|
157
|
+
"chainId": chain->ChainMap.Chain.toChainId,
|
|
158
|
+
"partitionId": query.partitionId,
|
|
159
|
+
"fromBlock": fromBlockQueried,
|
|
160
|
+
"toBlock": latestFetchedBlockNumber,
|
|
161
|
+
"numEvents": parsedQueueItems->Array.length,
|
|
162
|
+
})
|
|
163
|
+
} else {
|
|
164
|
+
Logging.trace({
|
|
165
|
+
"msg": "Finished querying",
|
|
166
|
+
"chainId": chain->ChainMap.Chain.toChainId,
|
|
167
|
+
"partitionId": query.partitionId,
|
|
168
|
+
"fromBlock": fromBlockQueried,
|
|
169
|
+
"toBlock": latestFetchedBlockNumber,
|
|
170
|
+
"numEvents": parsedQueueItems->Array.length,
|
|
171
|
+
"numContractRegisterEvents": numContractRegisterEvents,
|
|
172
|
+
})
|
|
173
|
+
}
|
|
165
174
|
|
|
166
175
|
let reorgResult = chainState->ChainState.registerReorgGuard(~blockHashes, ~knownHeight)
|
|
167
176
|
|
|
@@ -111,15 +111,26 @@ async function onQueryResponse(state, param, stateId, scheduleFetch, schedulePro
|
|
|
111
111
|
return count;
|
|
112
112
|
}
|
|
113
113
|
});
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
114
|
+
if (numContractRegisterEvents === 0) {
|
|
115
|
+
Logging.trace({
|
|
116
|
+
msg: "Finished querying",
|
|
117
|
+
chainId: chain,
|
|
118
|
+
partitionId: query.partitionId,
|
|
119
|
+
fromBlock: fromBlockQueried,
|
|
120
|
+
toBlock: latestFetchedBlockNumber,
|
|
121
|
+
numEvents: parsedQueueItems.length
|
|
122
|
+
});
|
|
123
|
+
} else {
|
|
124
|
+
Logging.trace({
|
|
125
|
+
msg: "Finished querying",
|
|
126
|
+
chainId: chain,
|
|
127
|
+
partitionId: query.partitionId,
|
|
128
|
+
fromBlock: fromBlockQueried,
|
|
129
|
+
toBlock: latestFetchedBlockNumber,
|
|
130
|
+
numEvents: parsedQueueItems.length,
|
|
131
|
+
numContractRegisterEvents: numContractRegisterEvents
|
|
132
|
+
});
|
|
133
|
+
}
|
|
123
134
|
let reorgResult = ChainState.registerReorgGuard(chainState, response.blockHashes, knownHeight);
|
|
124
135
|
let rollbackWithReorgDetectedBlockNumber;
|
|
125
136
|
if (typeof reorgResult !== "object") {
|
package/src/ChainState.res
CHANGED
|
@@ -410,13 +410,17 @@ let isReady = (cs: t) => cs.timestampCaughtUpToHeadOrEndblock !== None
|
|
|
410
410
|
// nudge it.
|
|
411
411
|
let densityBlendWindow = 100.
|
|
412
412
|
|
|
413
|
-
// The last block this chain can fetch right now: the head
|
|
414
|
-
//
|
|
413
|
+
// The last block this chain can fetch right now: the lagged head
|
|
414
|
+
// (knownHeight - blockLag, the frontier when a reorg-threshold blockLag holds
|
|
415
|
+
// back the tip), or endBlock when it's below that head. Matching isFetchingAtHead's
|
|
416
|
+
// definition of the head is what lets a chain parked at its lagged head read
|
|
417
|
+
// 100% progress instead of looking behind against blocks it can't fetch.
|
|
415
418
|
let fetchCeiling = (cs: t) => {
|
|
416
419
|
let fetchState = cs.fetchState
|
|
420
|
+
let head = Pervasives.max(0, fetchState.knownHeight - fetchState.blockLag)
|
|
417
421
|
switch fetchState.endBlock {
|
|
418
|
-
| Some(endBlock) => Pervasives.min(endBlock,
|
|
419
|
-
| None =>
|
|
422
|
+
| Some(endBlock) => Pervasives.min(endBlock, head)
|
|
423
|
+
| None => head
|
|
420
424
|
}
|
|
421
425
|
}
|
|
422
426
|
|
|
@@ -466,12 +470,14 @@ let targetBlock = (cs: t, ~chainTargetItems: float) => {
|
|
|
466
470
|
}
|
|
467
471
|
|
|
468
472
|
// Block range that cross-chain progress alignment maps fractions over: from
|
|
469
|
-
// the
|
|
470
|
-
//
|
|
473
|
+
// the chain's startBlock to the last block it can fetch right now. The lower
|
|
474
|
+
// bound is deliberately static — anchoring it at firstEventBlock would make
|
|
475
|
+
// two chains sitting at the same block read different progress fractions
|
|
476
|
+
// depending on whether each has discovered its first event yet, so the
|
|
477
|
+
// anchor's line could map below a follower's own frontier and stall it.
|
|
471
478
|
let progressRange = (cs: t) => {
|
|
472
479
|
let fetchState = cs.fetchState
|
|
473
|
-
|
|
474
|
-
(lower, cs->fetchCeiling)
|
|
480
|
+
(fetchState.startBlock, cs->fetchCeiling)
|
|
475
481
|
}
|
|
476
482
|
|
|
477
483
|
// A degenerate range (chain already at or past its last block) maps to 1 so it
|
|
@@ -492,6 +498,12 @@ let blockAtProgress = (cs: t, ~progress) => {
|
|
|
492
498
|
lower + Math.ceil(progress *. (upper - lower)->Int.toFloat)->Float.toInt
|
|
493
499
|
}
|
|
494
500
|
|
|
501
|
+
// The fetch frontier as a fraction of the alignable range — what the
|
|
502
|
+
// cross-chain waterfall aligns other chains against. Based on blocks actually
|
|
503
|
+
// fetched, so it holds up even while this chain's queries are in flight.
|
|
504
|
+
let frontierProgress = (cs: t) =>
|
|
505
|
+
cs->progressAtBlock(~blockNumber=cs.fetchState->FetchState.bufferBlockNumber)
|
|
506
|
+
|
|
495
507
|
// Propose queries sized against this chain's target block. Called by
|
|
496
508
|
// CrossChainState's waterfall, furthest-behind chain first, with
|
|
497
509
|
// chainTargetItems set to whatever budget remains at that point and
|
package/src/ChainState.res.mjs
CHANGED
|
@@ -254,11 +254,12 @@ function isReady(cs) {
|
|
|
254
254
|
|
|
255
255
|
function fetchCeiling(cs) {
|
|
256
256
|
let fetchState = cs.fetchState;
|
|
257
|
+
let head = Primitive_int.max(0, fetchState.knownHeight - fetchState.blockLag | 0);
|
|
257
258
|
let endBlock = fetchState.endBlock;
|
|
258
259
|
if (endBlock !== undefined) {
|
|
259
|
-
return Primitive_int.min(endBlock,
|
|
260
|
+
return Primitive_int.min(endBlock, head);
|
|
260
261
|
} else {
|
|
261
|
-
return
|
|
262
|
+
return head;
|
|
262
263
|
}
|
|
263
264
|
}
|
|
264
265
|
|
|
@@ -300,14 +301,20 @@ function targetBlock(cs, chainTargetItems) {
|
|
|
300
301
|
|
|
301
302
|
function progressRange(cs) {
|
|
302
303
|
let fetchState = cs.fetchState;
|
|
303
|
-
let lower = Stdlib_Option.getOr(fetchState.firstEventBlock, fetchState.startBlock);
|
|
304
304
|
return [
|
|
305
|
-
|
|
305
|
+
fetchState.startBlock,
|
|
306
306
|
fetchCeiling(cs)
|
|
307
307
|
];
|
|
308
308
|
}
|
|
309
309
|
|
|
310
|
-
function
|
|
310
|
+
function blockAtProgress(cs, progress) {
|
|
311
|
+
let match = progressRange(cs);
|
|
312
|
+
let lower = match[0];
|
|
313
|
+
return lower + (Math.ceil(progress * (match[1] - lower | 0)) | 0) | 0;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function frontierProgress(cs) {
|
|
317
|
+
let blockNumber = FetchState.bufferBlockNumber(cs.fetchState);
|
|
311
318
|
let match = progressRange(cs);
|
|
312
319
|
let upper = match[1];
|
|
313
320
|
let lower = match[0];
|
|
@@ -318,12 +325,6 @@ function progressAtBlock(cs, blockNumber) {
|
|
|
318
325
|
}
|
|
319
326
|
}
|
|
320
327
|
|
|
321
|
-
function blockAtProgress(cs, progress) {
|
|
322
|
-
let match = progressRange(cs);
|
|
323
|
-
let lower = match[0];
|
|
324
|
-
return lower + (Math.ceil(progress * (match[1] - lower | 0)) | 0) | 0;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
328
|
function getNextQuery(cs, chainTargetItems, chunkItemsMultiplierOpt, maxTargetBlock) {
|
|
328
329
|
let chunkItemsMultiplier = chunkItemsMultiplierOpt !== undefined ? chunkItemsMultiplierOpt : 1;
|
|
329
330
|
let chainTargetBlock = targetBlock(cs, chainTargetItems);
|
|
@@ -782,8 +783,8 @@ export {
|
|
|
782
783
|
hasReadyItem,
|
|
783
784
|
isReadyToEnterReorgThreshold,
|
|
784
785
|
targetBlock,
|
|
785
|
-
progressAtBlock,
|
|
786
786
|
blockAtProgress,
|
|
787
|
+
frontierProgress,
|
|
787
788
|
getNextQuery,
|
|
788
789
|
dispatch,
|
|
789
790
|
toChainData,
|
package/src/ChainState.resi
CHANGED
|
@@ -67,8 +67,8 @@ let isReadyToEnterReorgThreshold: t => bool
|
|
|
67
67
|
|
|
68
68
|
// Fetch control.
|
|
69
69
|
let targetBlock: (t, ~chainTargetItems: float) => int
|
|
70
|
-
let progressAtBlock: (t, ~blockNumber: int) => float
|
|
71
70
|
let blockAtProgress: (t, ~progress: float) => int
|
|
71
|
+
let frontierProgress: t => float
|
|
72
72
|
let getNextQuery: (
|
|
73
73
|
t,
|
|
74
74
|
~chainTargetItems: float,
|
package/src/CrossChainState.res
CHANGED
|
@@ -169,13 +169,18 @@ let applyBatchProgress = (crossChainState: t, ~batch: Batch.t, ~blockTimestampNa
|
|
|
169
169
|
|
|
170
170
|
// --- Fetch control. ---
|
|
171
171
|
|
|
172
|
-
// Chains ordered furthest-behind first
|
|
173
|
-
// chains with the most backfill work
|
|
172
|
+
// Chains ordered furthest-behind first by fetch-frontier progress, so the
|
|
173
|
+
// shared buffer pool goes to the chains with the most fetchable backfill work
|
|
174
|
+
// before the rest — and the same metric that sets the alignment line also
|
|
175
|
+
// decides who draws budget first, so the anchor is served before any chain it
|
|
176
|
+
// clamps. (Batch ordering keeps its own getProgressPercentage measure.) A chain
|
|
177
|
+
// with no known height reads 100% here and sorts last, which is fine: it can't
|
|
178
|
+
// fetch until its first block lands regardless of when it's visited.
|
|
174
179
|
let priorityOrder = (crossChainState: t) =>
|
|
175
180
|
crossChainState.chainStates
|
|
176
181
|
->Dict.valuesToArray
|
|
177
182
|
->Array.toSorted((a, b) =>
|
|
178
|
-
Float.compare(a->ChainState.
|
|
183
|
+
Float.compare(a->ChainState.frontierProgress, b->ChainState.frontierProgress)
|
|
179
184
|
)
|
|
180
185
|
|
|
181
186
|
// In-flight estimated items across every chain — the live draw against
|
|
@@ -198,69 +203,88 @@ let totalReservedSize = (crossChainState: t) => {
|
|
|
198
203
|
// chain-density-derived target block, then subtract what it actually used
|
|
199
204
|
// before moving to the next chain. So a chain that can only use a little
|
|
200
205
|
// (density too low, or already caught up) leaves the rest for the others
|
|
201
|
-
// automatically.
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
// prioritizing
|
|
206
|
+
// automatically. Starting a new query requires at least 10% of the target pool
|
|
207
|
+
// to be free. A chain visited after the budget falls below that admission unit
|
|
208
|
+
// doesn't query this round — reservations release as responses land, so the
|
|
209
|
+
// next tick redistributes. Every other chain is additionally capped at the
|
|
210
|
+
// lowest-frontier-progress chain's progress mapped onto its own range, so no
|
|
211
|
+
// chain runs ahead of the chain the pool is prioritizing — including on ticks
|
|
212
|
+
// where that chain is mid-fetch and emits no new query. A chain with no known
|
|
213
|
+
// height can't anchor this line (there's no range to measure against), a chain
|
|
214
|
+
// caught up to its fetchable head reads 100% and so never anchors while another
|
|
215
|
+
// is behind, and once the whole indexer has caught up (isRealtime) the clamp is
|
|
216
|
+
// dropped — chains at head only trail each other by real-time block production.
|
|
207
217
|
let checkAndFetch = async (
|
|
208
218
|
crossChainState: t,
|
|
209
219
|
~dispatchChain: (~chain: ChainMap.Chain.t, ~action: FetchState.nextQuery) => promise<unit>,
|
|
210
220
|
) => {
|
|
221
|
+
let targetBudget = crossChainState.targetBufferSize->Int.toFloat
|
|
211
222
|
let remaining = ref(
|
|
212
223
|
Pervasives.max(
|
|
213
224
|
0.,
|
|
214
|
-
|
|
225
|
+
targetBudget -.
|
|
215
226
|
crossChainState->totalReadyCount->Int.toFloat -.
|
|
216
227
|
crossChainState->totalReservedSize,
|
|
217
228
|
),
|
|
218
229
|
)
|
|
219
230
|
|
|
231
|
+
// New fetch work is admitted in units of 10% of the target pool. Waiting
|
|
232
|
+
// below this floor avoids spending the last few free items on undersized
|
|
233
|
+
// queries; response and batch completion schedule another tick after they
|
|
234
|
+
// release enough budget.
|
|
235
|
+
let minimumAdmissionBudget = targetBudget *. 0.1
|
|
236
|
+
|
|
220
237
|
// A chain with no density signal probes blind, so it only gets a bounded
|
|
221
238
|
// slice of the pool — one unknown chain shouldn't hold the whole budget
|
|
222
|
-
// while it takes its first measurements.
|
|
223
|
-
let coldChainBudget =
|
|
239
|
+
// while it takes its first measurements. Its probe is one admission unit.
|
|
240
|
+
let coldChainBudget = minimumAdmissionBudget
|
|
224
241
|
|
|
225
242
|
// Chunk reservations get headroom over the density estimate so a
|
|
226
243
|
// denser-than-expected range doesn't truncate at the server cap; realtime
|
|
227
244
|
// gets more since a forced catch-up query there costs a head-poll roundtrip.
|
|
228
245
|
let chunkItemsMultiplier = crossChainState.isRealtime ? 3. : 1.5
|
|
229
246
|
|
|
247
|
+
let prioritizedChainStates = crossChainState->priorityOrder
|
|
248
|
+
|
|
249
|
+
// Alignment anchor: the first known-height chain in priority order — which,
|
|
250
|
+
// since that order sorts by frontier progress, is the chain furthest behind
|
|
251
|
+
// by the very metric the clamp maps other chains against. Anchoring on the
|
|
252
|
+
// frontier (not on the target of whichever chain happens to query this tick)
|
|
253
|
+
// keeps the line in place while the anchor's queries are still in flight. A
|
|
254
|
+
// chain caught up to its fetchable head reads 100% and sorts past any behind
|
|
255
|
+
// chain, so it never anchors while another chain is behind.
|
|
256
|
+
let alignment = crossChainState.isRealtime
|
|
257
|
+
? None
|
|
258
|
+
: prioritizedChainStates
|
|
259
|
+
->Array.find(cs => cs->ChainState.knownHeight != 0)
|
|
260
|
+
->Option.map(cs => ((cs->ChainState.chainConfig).id, cs->ChainState.frontierProgress))
|
|
261
|
+
|
|
230
262
|
let actionByChain = Dict.make()
|
|
231
|
-
|
|
232
|
-
crossChainState
|
|
233
|
-
->priorityOrder
|
|
234
|
-
->Array.forEach(cs => {
|
|
263
|
+
prioritizedChainStates->Array.forEach(cs => {
|
|
235
264
|
let chainId = (cs->ChainState.chainConfig).id
|
|
236
|
-
if
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
|
|
265
|
+
if remaining.contents < minimumAdmissionBudget {
|
|
266
|
+
// More than 90% of the target pool is ready or reserved. Do not attempt
|
|
267
|
+
// any chain action until a full admission unit becomes free.
|
|
268
|
+
actionByChain->Utils.Dict.setByInt(chainId, FetchState.NothingToQuery)
|
|
269
|
+
} else if cs->ChainState.knownHeight == 0 {
|
|
270
|
+
// Let getNextQuery own the waiting decision without trying to size work
|
|
271
|
+
// against an unknown height.
|
|
272
|
+
actionByChain->Utils.Dict.setByInt(
|
|
273
|
+
chainId,
|
|
274
|
+
cs->ChainState.getNextQuery(~chainTargetItems=0., ~chunkItemsMultiplier),
|
|
275
|
+
)
|
|
241
276
|
} else {
|
|
242
277
|
let isCold = cs->ChainState.effectiveDensity === None
|
|
243
278
|
let chainTargetItems =
|
|
244
279
|
(isCold ? Pervasives.min(remaining.contents, coldChainBudget) : remaining.contents) +.
|
|
245
280
|
cs->ChainState.pendingBudget
|
|
246
|
-
let maxTargetBlock = switch
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
maxProgress :=
|
|
254
|
-
Some(
|
|
255
|
-
cs->ChainState.progressAtBlock(
|
|
256
|
-
~blockNumber=cs->ChainState.targetBlock(~chainTargetItems),
|
|
257
|
-
),
|
|
258
|
-
)
|
|
259
|
-
None
|
|
260
|
-
// 5% margin past the leader's line: chains whose progress tracks the
|
|
261
|
-
// leader closely would otherwise flap in and out of the clamp as
|
|
262
|
-
// leadership alternates, stalling their pipeline every other tick.
|
|
263
|
-
| Some(progress) => Some(cs->ChainState.blockAtProgress(~progress=progress +. 0.05))
|
|
281
|
+
let maxTargetBlock = switch alignment {
|
|
282
|
+
// 5% margin past the anchor's line: chains whose progress tracks the
|
|
283
|
+
// anchor closely would otherwise flap in and out of the clamp on every
|
|
284
|
+
// small frontier move, stalling their pipeline every other tick.
|
|
285
|
+
| Some((anchorChainId, progress)) if anchorChainId !== chainId =>
|
|
286
|
+
Some(cs->ChainState.blockAtProgress(~progress=progress +. 0.05))
|
|
287
|
+
| _ => None
|
|
264
288
|
}
|
|
265
289
|
switch cs->ChainState.getNextQuery(
|
|
266
290
|
~chainTargetItems,
|
|
@@ -274,8 +298,7 @@ let checkAndFetch = async (
|
|
|
274
298
|
// range to nothing — and has nothing else to wake it must keep polling
|
|
275
299
|
// for new blocks instead of going silent. NothingToQuery isn't
|
|
276
300
|
// dispatched, so such a chain would never be re-scheduled and its head
|
|
277
|
-
// tracking would freeze
|
|
278
|
-
// for the same reason). A chain is genuinely idle — and correctly left
|
|
301
|
+
// tracking would freeze. A chain is genuinely idle — and correctly left
|
|
279
302
|
// undispatched — when it is caught up to its head/endblock, still
|
|
280
303
|
// draining in-flight queries, or holding ready items that batch
|
|
281
304
|
// processing will drain and re-schedule from.
|
|
@@ -8,6 +8,7 @@ import * as ChainMap from "./ChainMap.res.mjs";
|
|
|
8
8
|
import * as ChainState from "./ChainState.res.mjs";
|
|
9
9
|
import * as Prometheus from "./Prometheus.res.mjs";
|
|
10
10
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
11
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
11
12
|
import * as Primitive_float from "@rescript/runtime/lib/es6/Primitive_float.js";
|
|
12
13
|
import * as SafeCheckpointTracking from "./SafeCheckpointTracking.res.mjs";
|
|
13
14
|
|
|
@@ -121,7 +122,7 @@ function applyBatchProgress(crossChainState, batch, blockTimestampName) {
|
|
|
121
122
|
}
|
|
122
123
|
|
|
123
124
|
function priorityOrder(crossChainState) {
|
|
124
|
-
return Object.values(crossChainState.chainStates).toSorted((a, b) => Primitive_float.compare(ChainState.
|
|
125
|
+
return Object.values(crossChainState.chainStates).toSorted((a, b) => Primitive_float.compare(ChainState.frontierProgress(a), ChainState.frontierProgress(b)));
|
|
125
126
|
}
|
|
126
127
|
|
|
127
128
|
function totalReservedSize(crossChainState) {
|
|
@@ -134,29 +135,33 @@ function totalReservedSize(crossChainState) {
|
|
|
134
135
|
}
|
|
135
136
|
|
|
136
137
|
async function checkAndFetch(crossChainState, dispatchChain) {
|
|
138
|
+
let targetBudget = crossChainState.targetBufferSize;
|
|
137
139
|
let remaining = {
|
|
138
|
-
contents: Primitive_float.max(0,
|
|
140
|
+
contents: Primitive_float.max(0, targetBudget - totalReadyCount(crossChainState) - totalReservedSize(crossChainState))
|
|
139
141
|
};
|
|
140
|
-
let
|
|
142
|
+
let minimumAdmissionBudget = targetBudget * 0.1;
|
|
141
143
|
let chunkItemsMultiplier = crossChainState.isRealtime ? 3 : 1.5;
|
|
144
|
+
let prioritizedChainStates = priorityOrder(crossChainState);
|
|
145
|
+
let alignment = crossChainState.isRealtime ? undefined : Stdlib_Option.map(prioritizedChainStates.find(cs => ChainState.knownHeight(cs) !== 0), cs => [
|
|
146
|
+
ChainState.chainConfig(cs).id,
|
|
147
|
+
ChainState.frontierProgress(cs)
|
|
148
|
+
]);
|
|
142
149
|
let actionByChain = {};
|
|
143
|
-
|
|
144
|
-
contents: undefined
|
|
145
|
-
};
|
|
146
|
-
priorityOrder(crossChainState).forEach(cs => {
|
|
150
|
+
prioritizedChainStates.forEach(cs => {
|
|
147
151
|
let chainId = ChainState.chainConfig(cs).id;
|
|
152
|
+
if (remaining.contents < minimumAdmissionBudget) {
|
|
153
|
+
actionByChain[chainId] = "NothingToQuery";
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
148
156
|
if (ChainState.knownHeight(cs) === 0) {
|
|
149
|
-
actionByChain[chainId] =
|
|
157
|
+
actionByChain[chainId] = ChainState.getNextQuery(cs, 0, chunkItemsMultiplier, undefined);
|
|
150
158
|
return;
|
|
151
159
|
}
|
|
152
160
|
let isCold = ChainState.effectiveDensity(cs) === undefined;
|
|
153
161
|
let chainTargetItems = (
|
|
154
|
-
isCold ? Primitive_float.min(remaining.contents,
|
|
162
|
+
isCold ? Primitive_float.min(remaining.contents, minimumAdmissionBudget) : remaining.contents
|
|
155
163
|
) + ChainState.pendingBudget(cs);
|
|
156
|
-
let
|
|
157
|
-
let maxTargetBlock = progress !== undefined ? ChainState.blockAtProgress(cs, progress + 0.05) : (
|
|
158
|
-
isCold ? undefined : (maxProgress.contents = ChainState.progressAtBlock(cs, ChainState.targetBlock(cs, chainTargetItems)), undefined)
|
|
159
|
-
);
|
|
164
|
+
let maxTargetBlock = alignment !== undefined && alignment[0] !== chainId ? ChainState.blockAtProgress(cs, alignment[1] + 0.05) : undefined;
|
|
160
165
|
let action = ChainState.getNextQuery(cs, chainTargetItems, chunkItemsMultiplier, maxTargetBlock);
|
|
161
166
|
if (typeof action !== "object") {
|
|
162
167
|
if (action === "WaitingForNewBlock") {
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Owns all per-(effect, scope) runtime state and its lifecycle. The two maps
|
|
2
|
+
// have different rollback semantics, so keeping them together behind an explicit
|
|
3
|
+
// `resetForRollback` makes the invariant enforced rather than remembered.
|
|
4
|
+
|
|
5
|
+
// Per-scope rate-limit window and queue. A chain-scoped effect gets one of
|
|
6
|
+
// these per chain, so each chain's throughput is independent.
|
|
7
|
+
type effectRateLimitState = {
|
|
8
|
+
callsPerDuration: int,
|
|
9
|
+
durationMs: int,
|
|
10
|
+
mutable availableCalls: int,
|
|
11
|
+
mutable windowStartTime: float,
|
|
12
|
+
mutable queueCount: int,
|
|
13
|
+
mutable nextWindowPromise: option<promise<unit>>,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type effectCacheInMemTable = {
|
|
17
|
+
// Cache keys whose handler output is persisted on the next write. Drained
|
|
18
|
+
// each write; eviction is driven by the per-entry checkpointId instead.
|
|
19
|
+
mutable idsToStore: array<string>,
|
|
20
|
+
mutable invalidationsCount: int,
|
|
21
|
+
// Each entry is stamped with the checkpoint that referenced it (or
|
|
22
|
+
// loadedFromDbCheckpointId for db reads), so committed entries can be
|
|
23
|
+
// dropped once persisted/re-derivable, mirroring entity changes.
|
|
24
|
+
mutable dict: dict<Change.t<Internal.effectOutput>>,
|
|
25
|
+
mutable changesCount: float,
|
|
26
|
+
effect: Internal.effect,
|
|
27
|
+
// The scope this cache is for and its resolved db table (the address). The
|
|
28
|
+
// in-mem table is keyed by `table.tableName`, so a cross-chain and a
|
|
29
|
+
// chain-scoped cache for the same effect stay isolated.
|
|
30
|
+
scope: Internal.chainScope,
|
|
31
|
+
table: Table.table,
|
|
32
|
+
rateLimitState: option<effectRateLimitState>,
|
|
33
|
+
// Per-scope active-call bookkeeping, surfaced through the Effect metrics.
|
|
34
|
+
mutable activeCallsCount: int,
|
|
35
|
+
mutable prevCallStartTimerRef: Performance.timeRef,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type t = {
|
|
39
|
+
// Cache tables keyed by cache table name. Dropped on rollback — the cache
|
|
40
|
+
// is re-derivable from the db.
|
|
41
|
+
mutable tables: dict<effectCacheInMemTable>,
|
|
42
|
+
// Rate-limit windows keyed by the same name. Survive rollback: rate limiting
|
|
43
|
+
// reflects real API throughput, not indexing progress, so a reorg must not
|
|
44
|
+
// refill an effect's budget.
|
|
45
|
+
rateLimits: dict<effectRateLimitState>,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let make = (): t => {tables: Dict.make(), rateLimits: Dict.make()}
|
|
49
|
+
|
|
50
|
+
let getRateLimitState = (self: t, ~tableName, ~effect: Internal.effect) =>
|
|
51
|
+
switch effect.rateLimit {
|
|
52
|
+
| None => None
|
|
53
|
+
| Some({callsPerDuration, durationMs}) =>
|
|
54
|
+
Some(
|
|
55
|
+
switch self.rateLimits->Utils.Dict.dangerouslyGetNonOption(tableName) {
|
|
56
|
+
| Some(existing) => existing
|
|
57
|
+
| None =>
|
|
58
|
+
let created: effectRateLimitState = {
|
|
59
|
+
callsPerDuration,
|
|
60
|
+
durationMs,
|
|
61
|
+
availableCalls: callsPerDuration,
|
|
62
|
+
windowStartTime: Date.now(),
|
|
63
|
+
queueCount: 0,
|
|
64
|
+
nextWindowPromise: None,
|
|
65
|
+
}
|
|
66
|
+
self.rateLimits->Dict.set(tableName, created)
|
|
67
|
+
created
|
|
68
|
+
},
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Get, or lazily create, the in-mem table for an (effect, scope). A recreated
|
|
73
|
+
// table (e.g. after a rollback) reuses the surviving rate-limit window.
|
|
74
|
+
let getTable = (self: t, ~effect: Internal.effect, ~scope: Internal.chainScope) => {
|
|
75
|
+
let tableName = Internal.EffectCache.toTableName(~effectName=effect.name, ~scope)
|
|
76
|
+
switch self.tables->Utils.Dict.dangerouslyGetNonOption(tableName) {
|
|
77
|
+
| Some(inMemTable) => inMemTable
|
|
78
|
+
| None =>
|
|
79
|
+
let inMemTable: effectCacheInMemTable = {
|
|
80
|
+
idsToStore: [],
|
|
81
|
+
dict: Dict.make(),
|
|
82
|
+
changesCount: 0.,
|
|
83
|
+
invalidationsCount: 0,
|
|
84
|
+
effect,
|
|
85
|
+
scope,
|
|
86
|
+
table: Internal.makeCacheTable(~effectName=effect.name, ~scope),
|
|
87
|
+
rateLimitState: self->getRateLimitState(~tableName, ~effect),
|
|
88
|
+
activeCallsCount: 0,
|
|
89
|
+
prevCallStartTimerRef: %raw(`null`),
|
|
90
|
+
}
|
|
91
|
+
self.tables->Dict.set(tableName, inMemTable)
|
|
92
|
+
inMemTable
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let forEach = (self: t, fn) => self.tables->Utils.Dict.forEach(fn)
|
|
97
|
+
|
|
98
|
+
// Drop the cache tables (re-derivable from the db) but keep the rate-limit
|
|
99
|
+
// windows so a reorg doesn't hand an effect a fresh budget.
|
|
100
|
+
let resetForRollback = (self: t) => self.tables = Dict.make()
|