envio 3.3.0-alpha.0 → 3.3.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "envio",
3
- "version": "3.3.0-alpha.0",
3
+ "version": "3.3.0-alpha.1",
4
4
  "type": "module",
5
5
  "description": "A latency and sync speed optimized, developer friendly blockchain data indexer.",
6
6
  "bin": "./bin.mjs",
@@ -69,10 +69,10 @@
69
69
  "tsx": "4.21.0"
70
70
  },
71
71
  "optionalDependencies": {
72
- "envio-linux-x64": "3.3.0-alpha.0",
73
- "envio-linux-x64-musl": "3.3.0-alpha.0",
74
- "envio-linux-arm64": "3.3.0-alpha.0",
75
- "envio-darwin-x64": "3.3.0-alpha.0",
76
- "envio-darwin-arm64": "3.3.0-alpha.0"
72
+ "envio-linux-x64": "3.3.0-alpha.1",
73
+ "envio-linux-x64-musl": "3.3.0-alpha.1",
74
+ "envio-linux-arm64": "3.3.0-alpha.1",
75
+ "envio-darwin-x64": "3.3.0-alpha.1",
76
+ "envio-darwin-arm64": "3.3.0-alpha.1"
77
77
  }
78
78
  }
@@ -308,8 +308,7 @@ let finishWaitingForNewBlock = (
308
308
  let fetchChain = async (
309
309
  state: IndexerState.t,
310
310
  chain,
311
- ~concurrencyLimit,
312
- ~bufferLimit,
311
+ ~action,
313
312
  ~stateId,
314
313
  ~scheduleFetch,
315
314
  ~scheduleProcessing,
@@ -321,14 +320,14 @@ let fetchChain = async (
321
320
  let isRealtime = state->IndexerState.isRealtime
322
321
  let sourceManager = chainState->ChainState.sourceManager
323
322
 
324
- // Only affects the WaitingForNewBlock branch of fetchNext, where
323
+ // Only affects the WaitingForNewBlock branch of dispatch, where
325
324
  // there's nothing to fetch. During backfill any such chain is idle.
326
325
  let reducedPolling = !isRealtime
327
326
 
328
327
  // Owns its error boundary: launch doesn't catch, so any failure here (the
329
- // query, response handling, or fetchNext itself) must stop the indexer.
328
+ // query, response handling, or dispatch itself) must stop the indexer.
330
329
  try {
331
- await sourceManager->SourceManager.fetchNext(
330
+ await sourceManager->SourceManager.dispatch(
332
331
  ~fetchState,
333
332
  ~waitForNewBlock=(~knownHeight) =>
334
333
  sourceManager->SourceManager.waitForNewBlock(~knownHeight, ~isRealtime, ~reducedPolling),
@@ -343,7 +342,7 @@ let fetchChain = async (
343
342
  ),
344
343
  ~executeQuery=async query => {
345
344
  // Caught here (not just by the outer try) so the query promise never
346
- // rejects: fetchNext spins a side-chain off it that would otherwise
345
+ // rejects: dispatch spins a side-chain off it that would otherwise
347
346
  // become an unhandled rejection.
348
347
  try {
349
348
  let response = await sourceManager->SourceManager.executeQuery(
@@ -363,8 +362,7 @@ let fetchChain = async (
363
362
  | exn => IndexerState.errorExit(state, exn->ErrorHandling.make)
364
363
  }
365
364
  },
366
- ~concurrencyLimit,
367
- ~bufferLimit,
365
+ ~action,
368
366
  ~stateId,
369
367
  )
370
368
  } catch {
@@ -167,7 +167,7 @@ function finishWaitingForNewBlock(state, chain, knownHeight, stateId, scheduleFe
167
167
  scheduleProcessing();
168
168
  }
169
169
 
170
- async function fetchChain(state, chain, concurrencyLimit, bufferLimit, stateId, scheduleFetch, scheduleProcessing, scheduleRollback) {
170
+ async function fetchChain(state, chain, action, stateId, scheduleFetch, scheduleProcessing, scheduleRollback) {
171
171
  let chainState = IndexerState.getChainState(state, chain);
172
172
  if (IndexerState.isResolvingReorg(state) || IndexerState.isStopped(state)) {
173
173
  return;
@@ -177,7 +177,7 @@ async function fetchChain(state, chain, concurrencyLimit, bufferLimit, stateId,
177
177
  let sourceManager = ChainState.sourceManager(chainState);
178
178
  let reducedPolling = !isRealtime;
179
179
  try {
180
- return await SourceManager.fetchNext(sourceManager, fetchState, async query => {
180
+ return await SourceManager.dispatch(sourceManager, fetchState, async query => {
181
181
  try {
182
182
  let response = await SourceManager.executeQuery(sourceManager, query, fetchState.knownHeight, isRealtime);
183
183
  return await onQueryResponse(state, {
@@ -189,7 +189,7 @@ async function fetchChain(state, chain, concurrencyLimit, bufferLimit, stateId,
189
189
  let exn = Primitive_exceptions.internalToException(raw_exn);
190
190
  return IndexerState.errorExit(state, ErrorHandling.make(exn, undefined, undefined));
191
191
  }
192
- }, knownHeight => SourceManager.waitForNewBlock(sourceManager, knownHeight, isRealtime, reducedPolling), knownHeight => finishWaitingForNewBlock(state, chain, knownHeight, stateId, scheduleFetch, scheduleProcessing), concurrencyLimit, bufferLimit, stateId);
192
+ }, knownHeight => SourceManager.waitForNewBlock(sourceManager, knownHeight, isRealtime, reducedPolling), knownHeight => finishWaitingForNewBlock(state, chain, knownHeight, stateId, scheduleFetch, scheduleProcessing), action, stateId);
193
193
  } catch (raw_exn) {
194
194
  let exn = Primitive_exceptions.internalToException(raw_exn);
195
195
  return IndexerState.errorExit(state, ErrorHandling.make(exn, undefined, IndexerState.unexpectedErrorMsg));
@@ -11,6 +11,10 @@ type t = {
11
11
  mutable timestampCaughtUpToHeadOrEndblock: option<Date.t>,
12
12
  mutable committedProgressBlockNumber: int,
13
13
  mutable numEventsProcessed: float,
14
+ // Running sum of in-flight queries' estResponseSize, kept here so the
15
+ // scheduler doesn't re-sum pending queries on every tick. Incremented when
16
+ // queries are dispatched, decremented as their responses land.
17
+ mutable pendingBudget: float,
14
18
  mutable reorgDetection: ReorgDetection.t,
15
19
  mutable safeCheckpointTracking: option<SafeCheckpointTracking.t>,
16
20
  }
@@ -49,6 +53,7 @@ let make = (
49
53
  timestampCaughtUpToHeadOrEndblock,
50
54
  committedProgressBlockNumber,
51
55
  numEventsProcessed,
56
+ pendingBudget: 0.,
52
57
  reorgDetection,
53
58
  safeCheckpointTracking,
54
59
  }
@@ -380,8 +385,25 @@ let safeCheckpointTracking = (cs: t) => cs.safeCheckpointTracking
380
385
  let isProgressAtHead = (cs: t) => cs.isProgressAtHead
381
386
  let committedProgressBlockNumber = (cs: t) => cs.committedProgressBlockNumber
382
387
  let numEventsProcessed = (cs: t) => cs.numEventsProcessed
388
+ let pendingBudget = (cs: t) => cs.pendingBudget
383
389
  let timestampCaughtUpToHeadOrEndblock = (cs: t) => cs.timestampCaughtUpToHeadOrEndblock
384
390
 
391
+ // Mark queries as in flight and reserve their estimated size against the shared
392
+ // buffer budget in one step, so the counter stays in sync with the pending
393
+ // queries it tracks.
394
+ let startFetchingQueries = (cs: t, ~queries: array<FetchState.query>) => {
395
+ cs.fetchState->FetchState.startFetchingQueries(~queries)
396
+ cs.pendingBudget =
397
+ cs.pendingBudget +. queries->Array.reduce(0., (acc, query) => acc +. query.estResponseSize)
398
+ }
399
+
400
+ // Drop every in-flight query and release their reservations together, keeping
401
+ // pendingBudget coupled to the pending queries it tracks.
402
+ let resetPendingQueries = (cs: t) => {
403
+ cs.fetchState = cs.fetchState->FetchState.resetPendingQueries
404
+ cs.pendingBudget = 0.
405
+ }
406
+
385
407
  // --- Derived (pure). ---
386
408
 
387
409
  let hasProcessedToEndblock = (cs: t) => {
@@ -426,6 +448,9 @@ let handleQueryResult = (
426
448
  fs
427
449
  ->FetchState.handleQueryResult(~query, ~latestFetchedBlock, ~newItems)
428
450
  ->FetchState.updateKnownHeight(~knownHeight)
451
+
452
+ // The query is no longer in flight, so release its reservation.
453
+ cs.pendingBudget = Pervasives.max(0., cs.pendingBudget -. query.estResponseSize)
429
454
  }
430
455
 
431
456
  // Run reorg detection against a fetch response and commit the updated guard.
@@ -447,7 +472,7 @@ let prepareReorg = (cs: t, ~eventsProcessedDiff) => {
447
472
  | Some(diff) => cs.numEventsProcessed = cs.numEventsProcessed +. diff
448
473
  | None => ()
449
474
  }
450
- cs.fetchState = cs.fetchState->FetchState.resetPendingQueries
475
+ cs->resetPendingQueries
451
476
  }
452
477
 
453
478
  let updateKnownHeight = (cs: t, ~knownHeight) =>
@@ -17,6 +17,7 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
17
17
  import * as ReorgDetection from "./ReorgDetection.res.mjs";
18
18
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
19
19
  import * as HyperFuelSource from "./sources/HyperFuelSource.res.mjs";
20
+ import * as Primitive_float from "@rescript/runtime/lib/es6/Primitive_float.js";
20
21
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
21
22
  import * as SvmHyperSyncSource from "./sources/SvmHyperSyncSource.res.mjs";
22
23
  import * as SafeCheckpointTracking from "./SafeCheckpointTracking.res.mjs";
@@ -49,6 +50,7 @@ function make(chainConfig, fetchState, sourceManager, reorgDetection, committedP
49
50
  timestampCaughtUpToHeadOrEndblock: timestampCaughtUpToHeadOrEndblock,
50
51
  committedProgressBlockNumber: committedProgressBlockNumber,
51
52
  numEventsProcessed: numEventsProcessed,
53
+ pendingBudget: 0,
52
54
  reorgDetection: reorgDetection,
53
55
  safeCheckpointTracking: safeCheckpointTracking
54
56
  };
@@ -225,10 +227,19 @@ function numEventsProcessed(cs) {
225
227
  return cs.numEventsProcessed;
226
228
  }
227
229
 
230
+ function pendingBudget(cs) {
231
+ return cs.pendingBudget;
232
+ }
233
+
228
234
  function timestampCaughtUpToHeadOrEndblock(cs) {
229
235
  return cs.timestampCaughtUpToHeadOrEndblock;
230
236
  }
231
237
 
238
+ function startFetchingQueries(cs, queries) {
239
+ FetchState.startFetchingQueries(cs.fetchState, queries);
240
+ cs.pendingBudget = cs.pendingBudget + Stdlib_Array.reduce(queries, 0, (acc, query) => acc + query.estResponseSize);
241
+ }
242
+
232
243
  function hasProcessedToEndblock(cs) {
233
244
  let fetchState = cs.fetchState;
234
245
  let committedProgressBlockNumber = cs.committedProgressBlockNumber;
@@ -264,6 +275,7 @@ function isFetchingAtHead(cs) {
264
275
  function handleQueryResult(cs, query, newItems, newItemsWithDcs, latestFetchedBlock, knownHeight) {
265
276
  let fs = newItemsWithDcs.length !== 0 ? FetchState.registerDynamicContracts(cs.fetchState, newItemsWithDcs) : cs.fetchState;
266
277
  cs.fetchState = FetchState.updateKnownHeight(FetchState.handleQueryResult(fs, query, latestFetchedBlock, newItems), knownHeight);
278
+ cs.pendingBudget = Primitive_float.max(0, cs.pendingBudget - query.estResponseSize);
267
279
  }
268
280
 
269
281
  function registerReorgGuard(cs, blockHashes, knownHeight) {
@@ -277,6 +289,7 @@ function prepareReorg(cs, eventsProcessedDiff) {
277
289
  cs.numEventsProcessed = cs.numEventsProcessed + eventsProcessedDiff;
278
290
  }
279
291
  cs.fetchState = FetchState.resetPendingQueries(cs.fetchState);
292
+ cs.pendingBudget = 0;
280
293
  }
281
294
 
282
295
  function updateKnownHeight(cs, knownHeight) {
@@ -441,6 +454,8 @@ export {
441
454
  isProgressAtHead,
442
455
  committedProgressBlockNumber,
443
456
  numEventsProcessed,
457
+ pendingBudget,
458
+ startFetchingQueries,
444
459
  timestampCaughtUpToHeadOrEndblock,
445
460
  hasProcessedToEndblock,
446
461
  getHighestBlockBelowThreshold,
@@ -46,6 +46,8 @@ let safeCheckpointTracking: t => option<SafeCheckpointTracking.t>
46
46
  let isProgressAtHead: t => bool
47
47
  let committedProgressBlockNumber: t => int
48
48
  let numEventsProcessed: t => float
49
+ let pendingBudget: t => float
50
+ let startFetchingQueries: (t, ~queries: array<FetchState.query>) => unit
49
51
  let timestampCaughtUpToHeadOrEndblock: t => option<Date.t>
50
52
 
51
53
  // Derived (pure).
@@ -11,10 +11,6 @@ type t = {
11
11
  // True once every chain has caught up to head/endBlock. Monotonic during a run.
12
12
  mutable isRealtime: bool,
13
13
  mutable isInReorgThreshold: bool,
14
- // Indexer-wide caps on concurrent data-source queries, shared across all
15
- // chains. The realtime budget applies once every chain is at head.
16
- maxBackfillConcurrency: int,
17
- maxRealtimeConcurrency: int,
18
14
  // Indexer-wide fetch buffer pool (item count), shared across all chains.
19
15
  targetBufferSize: int,
20
16
  }
@@ -26,18 +22,10 @@ let calculateTargetBufferSize = () =>
26
22
  | None => 100_000
27
23
  }
28
24
 
29
- // The concurrency budget in force for the current phase.
30
- let maxConcurrency = (crossChainState: t) =>
31
- crossChainState.isRealtime
32
- ? crossChainState.maxRealtimeConcurrency
33
- : crossChainState.maxBackfillConcurrency
34
-
35
25
  let make = (
36
26
  ~chainStates,
37
27
  ~isInReorgThreshold,
38
28
  ~isRealtime,
39
- ~maxBackfillConcurrency=Env.maxBackfillConcurrency,
40
- ~maxRealtimeConcurrency=Env.maxRealtimeConcurrency,
41
29
  ~targetBufferSize=calculateTargetBufferSize(),
42
30
  ): t => {
43
31
  let crossChainState = {
@@ -45,11 +33,8 @@ let make = (
45
33
  chainIds: chainStates->Dict.valuesToArray->Array.map(cs => (cs->ChainState.chainConfig).id),
46
34
  isRealtime,
47
35
  isInReorgThreshold,
48
- maxBackfillConcurrency,
49
- maxRealtimeConcurrency,
50
36
  targetBufferSize,
51
37
  }
52
- Prometheus.IndexingMaxConcurrency.set(~maxConcurrency=crossChainState->maxConcurrency)
53
38
  Prometheus.IndexingTargetBufferSize.set(~targetBufferSize)
54
39
  crossChainState
55
40
  }
@@ -65,17 +50,6 @@ let chainStates = (crossChainState: t) => crossChainState.chainStates
65
50
  let isRealtime = (crossChainState: t) => crossChainState.isRealtime
66
51
  let isInReorgThreshold = (crossChainState: t) => crossChainState.isInReorgThreshold
67
52
 
68
- // Partition queries in flight across every chain — the live draw against
69
- // maxConcurrency.
70
- let inFlight = (crossChainState: t) => {
71
- let total = ref(0)
72
- for i in 0 to crossChainState.chainIds->Array.length - 1 {
73
- let cs = crossChainState->getChainState(crossChainState.chainIds->Array.getUnsafe(i))
74
- total := total.contents + cs->ChainState.sourceManager->SourceManager.inFlightCount
75
- }
76
- total.contents
77
- }
78
-
79
53
  // Ready-to-process items across every chain — the live draw against
80
54
  // targetBufferSize, which is a budget of processable events (items stuck behind
81
55
  // a gap don't count toward the goal of keeping ~targetBufferSize ready).
@@ -199,18 +173,13 @@ let applyBatchProgress = (crossChainState: t, ~batch: Batch.t) => {
199
173
  Prometheus.ProgressReady.setAllReady()
200
174
  }
201
175
 
202
- let wasRealtime = crossChainState.isRealtime
203
176
  crossChainState.isRealtime = crossChainState.isRealtime || allChainsReady.contents
204
- if !wasRealtime && crossChainState.isRealtime {
205
- // The realtime budget takes over now that every chain is at head.
206
- Prometheus.IndexingMaxConcurrency.set(~maxConcurrency=crossChainState->maxConcurrency)
207
- }
208
177
  }
209
178
 
210
179
  // --- Fetch control. ---
211
180
 
212
- // Chains ordered furthest-behind first, so the shared concurrency and buffer
213
- // pools go to the chains with the most backfill work before the rest.
181
+ // Chains ordered furthest-behind first, so the shared buffer pool goes to the
182
+ // chains with the most backfill work before the rest.
214
183
  let priorityOrder = (crossChainState: t) =>
215
184
  crossChainState.chainStates
216
185
  ->Dict.valuesToArray
@@ -221,49 +190,105 @@ let priorityOrder = (crossChainState: t) =>
221
190
  )
222
191
  )
223
192
 
224
- // Dispatch a fetch tick across every chain in priority order, drawing from the
225
- // shared concurrency and buffer pools. Chains are visited in turn; fetchChain
226
- // bumps the in-flight count synchronously before it suspends, so a later chain
227
- // sees the slots an earlier one already claimed and the budget is honored
228
- // indexer-wide.
229
- //
230
- // bufferLimit is each chain's slice of the shared pool: it may grow its buffer
231
- // into whatever the other chains leave free, so a lone backfilling chain can use
232
- // the whole pool while head-following chains stay shallow.
193
+ // In-flight estimated items across every chain the live draw against
194
+ // targetBufferSize alongside totalReadyCount, so the pool isn't re-dispatched
195
+ // while queries are still being fetched.
196
+ let totalReservedSize = (crossChainState: t) => {
197
+ let total = ref(0.)
198
+ for i in 0 to crossChainState.chainIds->Array.length - 1 {
199
+ let cs = crossChainState->getChainState(crossChainState.chainIds->Array.getUnsafe(i))
200
+ total := total.contents +. cs->ChainState.pendingBudget
201
+ }
202
+ total.contents
203
+ }
204
+
205
+ // Furthest-behind first: order candidate queries by the chain progress % at
206
+ // their fromBlock, so the most behind ranges across all chains are admitted
207
+ // before the rest.
208
+ let compareByProgress = (a: FetchState.query, b: FetchState.query) =>
209
+ Float.compare(a.progress, b.progress)
210
+
211
+ // Dispatch a fetch tick across the whole indexer from one shared pool of
212
+ // ~targetBufferSize ready events. Every chain proposes its candidate queries
213
+ // (each carrying an estimated response size) against the full free budget; the
214
+ // candidates are then pooled, ordered by chain progress (furthest-behind first),
215
+ // and admitted until the budget is consumed. So the budget is split per query
216
+ // across chains rather than per chain — a chain that can only use a little
217
+ // leaves the rest for the others automatically.
233
218
  let checkAndFetch = async (
234
219
  crossChainState: t,
235
- ~fetchChain: (
236
- ~chain: ChainMap.Chain.t,
237
- ~concurrencyLimit: int,
238
- ~bufferLimit: int,
239
- ) => promise<unit>,
220
+ ~dispatchChain: (~chain: ChainMap.Chain.t, ~action: FetchState.nextQuery) => promise<unit>,
240
221
  ) => {
241
- let maxConcurrency = crossChainState->maxConcurrency
242
- // Pool is a budget of ready-to-process items, but the cap fed to getNextQuery
243
- // is still a buffer position (it tracks the sorted buffer, ready items first),
244
- // so add this chain's own buffer size back rather than its ready count. A chain
245
- // with a gap therefore gets extra headroom to fetch the not-ready overhang
246
- // while still aiming for its share of ~targetBufferSize ready events.
247
- let totalReady = crossChainState->totalReadyCount
248
- // Track the in-flight total as a running counter (summed once up front, then
249
- // adjusted by each chain's delta) instead of re-summing every chain — O(chains)
250
- // per tick rather than O(chains^2). fetchChain bumps the chain's count
251
- // synchronously, so the delta is observable right after the call.
252
- let inFlight = ref(crossChainState->inFlight)
253
- let priorityOrdered = crossChainState->priorityOrder
222
+ let remaining = Pervasives.max(
223
+ 0,
224
+ crossChainState.targetBufferSize -
225
+ crossChainState->totalReadyCount -
226
+ crossChainState->totalReservedSize->Float.toInt,
227
+ )
228
+
229
+ let chainIds = crossChainState.chainIds
230
+ let actionByChain = Dict.make()
231
+ // Candidate queries from every chain. Each query carries its chain id and the
232
+ // chain progress % at its fromBlock (the admission sort key), set here so the
233
+ // pool can be ordered without a side tuple per query.
234
+ let candidates = []
235
+ for i in 0 to chainIds->Array.length - 1 {
236
+ let chainId = chainIds->Array.getUnsafe(i)
237
+ let cs = crossChainState->getChainState(chainId)
238
+ let fetchState = cs->ChainState.fetchState
239
+ switch fetchState->FetchState.getNextQuery(
240
+ ~budget=remaining,
241
+ ~chainPendingBudget=cs->ChainState.pendingBudget,
242
+ ) {
243
+ | (WaitingForNewBlock | NothingToQuery) as action =>
244
+ actionByChain->Utils.Dict.setByInt(chainId, action)
245
+ | Ready(queries) =>
246
+ // Default to NothingToQuery; replaced below if any candidate is admitted.
247
+ actionByChain->Utils.Dict.setByInt(chainId, FetchState.NothingToQuery)
248
+ queries->Array.forEach(query => {
249
+ query.chainId = chainId
250
+ query.progress =
251
+ fetchState->FetchState.getProgressPercentageAt(~blockNumber=query.fromBlock)
252
+ candidates->Array.push(query)
253
+ })
254
+ }
255
+ }
256
+
257
+ candidates->Array.sort(compareByProgress)
258
+
259
+ // Admit furthest-behind first until the budget runs out. The condition is
260
+ // checked before each query, so as long as there's any budget left we admit
261
+ // the next one even when its estimate alone exceeds the remainder — otherwise a
262
+ // chain whose only query is bigger than the budget would never make progress.
263
+ let admittedByChain = Dict.make()
264
+ let running = ref(0.)
265
+ let remainingF = remaining->Int.toFloat
266
+ let idx = ref(0)
267
+ while running.contents < remainingF && idx.contents < candidates->Array.length {
268
+ let query = candidates->Array.getUnsafe(idx.contents)
269
+ admittedByChain->Utils.Dict.push(query.chainId->Int.toString, query)
270
+ running := running.contents +. query.estResponseSize
271
+ idx := idx.contents + 1
272
+ }
273
+ admittedByChain->Dict.forEachWithKey((queries, chainId) => {
274
+ actionByChain->Dict.set(chainId, FetchState.Ready(queries))
275
+ // Mark the admitted queries in flight and reserve their size against the
276
+ // shared budget; released as each response lands in handleQueryResult.
277
+ crossChainState
278
+ ->getChainState(chainId->Int.fromString->Option.getUnsafe)
279
+ ->ChainState.startFetchingQueries(~queries)
280
+ })
281
+
254
282
  let promises = []
255
- for i in 0 to priorityOrdered->Array.length - 1 {
256
- let cs = priorityOrdered->Array.getUnsafe(i)
257
- let chain = ChainMap.Chain.makeUnsafe(~chainId=(cs->ChainState.chainConfig).id)
258
- let sourceManager = cs->ChainState.sourceManager
259
- let concurrencyLimit = Pervasives.max(0, maxConcurrency - inFlight.contents)
260
- let bufferLimit =
261
- crossChainState.targetBufferSize -
262
- (totalReady -
263
- cs->ChainState.fetchState->FetchState.bufferSize)
264
- let inFlightBefore = sourceManager->SourceManager.inFlightCount
265
- promises->Array.push(fetchChain(~chain, ~concurrencyLimit, ~bufferLimit))
266
- inFlight := inFlight.contents + (sourceManager->SourceManager.inFlightCount - inFlightBefore)
283
+ for i in 0 to chainIds->Array.length - 1 {
284
+ let chainId = chainIds->Array.getUnsafe(i)
285
+ switch actionByChain->Utils.Dict.dangerouslyGetByIntNonOption(chainId) {
286
+ | Some(NothingToQuery)
287
+ | None => ()
288
+ | Some(action) =>
289
+ let chain = ChainMap.Chain.makeUnsafe(~chainId)
290
+ promises->Array.push(dispatchChain(~chain, ~action))
291
+ }
267
292
  }
268
293
  let _ = await promises->Promise.all
269
294
  }
@@ -8,8 +8,9 @@ import * as ChainMap from "./ChainMap.res.mjs";
8
8
  import * as ChainState from "./ChainState.res.mjs";
9
9
  import * as FetchState from "./FetchState.res.mjs";
10
10
  import * as Prometheus from "./Prometheus.res.mjs";
11
+ import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
12
+ import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
11
13
  import * as Primitive_int from "@rescript/runtime/lib/es6/Primitive_int.js";
12
- import * as SourceManager from "./sources/SourceManager.res.mjs";
13
14
  import * as Primitive_float from "@rescript/runtime/lib/es6/Primitive_float.js";
14
15
  import * as SafeCheckpointTracking from "./SafeCheckpointTracking.res.mjs";
15
16
 
@@ -21,17 +22,7 @@ function calculateTargetBufferSize() {
21
22
  }
22
23
  }
23
24
 
24
- function maxConcurrency(crossChainState) {
25
- if (crossChainState.isRealtime) {
26
- return crossChainState.maxRealtimeConcurrency;
27
- } else {
28
- return crossChainState.maxBackfillConcurrency;
29
- }
30
- }
31
-
32
- function make(chainStates, isInReorgThreshold, isRealtime, maxBackfillConcurrencyOpt, maxRealtimeConcurrencyOpt, targetBufferSizeOpt) {
33
- let maxBackfillConcurrency = maxBackfillConcurrencyOpt !== undefined ? maxBackfillConcurrencyOpt : Env.maxBackfillConcurrency;
34
- let maxRealtimeConcurrency = maxRealtimeConcurrencyOpt !== undefined ? maxRealtimeConcurrencyOpt : Env.maxRealtimeConcurrency;
25
+ function make(chainStates, isInReorgThreshold, isRealtime, targetBufferSizeOpt) {
35
26
  let targetBufferSize = targetBufferSizeOpt !== undefined ? targetBufferSizeOpt : (
36
27
  Env.targetBufferSize !== undefined ? Env.targetBufferSize : 100000
37
28
  );
@@ -40,11 +31,8 @@ function make(chainStates, isInReorgThreshold, isRealtime, maxBackfillConcurrenc
40
31
  chainIds: Object.values(chainStates).map(cs => ChainState.chainConfig(cs).id),
41
32
  isRealtime: isRealtime,
42
33
  isInReorgThreshold: isInReorgThreshold,
43
- maxBackfillConcurrency: maxBackfillConcurrency,
44
- maxRealtimeConcurrency: maxRealtimeConcurrency,
45
34
  targetBufferSize: targetBufferSize
46
35
  };
47
- Prometheus.IndexingMaxConcurrency.set(maxConcurrency(crossChainState));
48
36
  Prometheus.IndexingTargetBufferSize.set(targetBufferSize);
49
37
  return crossChainState;
50
38
  }
@@ -61,15 +49,6 @@ function isInReorgThreshold(crossChainState) {
61
49
  return crossChainState.isInReorgThreshold;
62
50
  }
63
51
 
64
- function inFlight(crossChainState) {
65
- let total = 0;
66
- for (let i = 0, i_finish = crossChainState.chainIds.length; i < i_finish; ++i) {
67
- let cs = crossChainState.chainStates[crossChainState.chainIds[i]];
68
- total = total + SourceManager.inFlightCount(ChainState.sourceManager(cs)) | 0;
69
- }
70
- return total;
71
- }
72
-
73
52
  function totalReadyCount(crossChainState) {
74
53
  let total = 0;
75
54
  for (let i = 0, i_finish = crossChainState.chainIds.length; i < i_finish; ++i) {
@@ -148,32 +127,79 @@ function applyBatchProgress(crossChainState, batch) {
148
127
  if (allChainsReady) {
149
128
  Prometheus.ProgressReady.setAllReady();
150
129
  }
151
- let wasRealtime = crossChainState.isRealtime;
152
130
  crossChainState.isRealtime = crossChainState.isRealtime || allChainsReady;
153
- if (!wasRealtime && crossChainState.isRealtime) {
154
- return Prometheus.IndexingMaxConcurrency.set(maxConcurrency(crossChainState));
155
- }
156
131
  }
157
132
 
158
133
  function priorityOrder(crossChainState) {
159
134
  return Object.values(crossChainState.chainStates).toSorted((a, b) => Primitive_float.compare(FetchState.getProgressPercentage(ChainState.fetchState(a)), FetchState.getProgressPercentage(ChainState.fetchState(b))));
160
135
  }
161
136
 
162
- async function checkAndFetch(crossChainState, fetchChain) {
163
- let maxConcurrency$1 = maxConcurrency(crossChainState);
164
- let totalReady = totalReadyCount(crossChainState);
165
- let inFlight$1 = inFlight(crossChainState);
166
- let priorityOrdered = priorityOrder(crossChainState);
137
+ function totalReservedSize(crossChainState) {
138
+ let total = 0;
139
+ for (let i = 0, i_finish = crossChainState.chainIds.length; i < i_finish; ++i) {
140
+ let cs = crossChainState.chainStates[crossChainState.chainIds[i]];
141
+ total = total + ChainState.pendingBudget(cs);
142
+ }
143
+ return total;
144
+ }
145
+
146
+ function compareByProgress(a, b) {
147
+ return Primitive_float.compare(a.progress, b.progress);
148
+ }
149
+
150
+ async function checkAndFetch(crossChainState, dispatchChain) {
151
+ let remaining = Primitive_int.max(0, (crossChainState.targetBufferSize - totalReadyCount(crossChainState) | 0) - (totalReservedSize(crossChainState) | 0) | 0);
152
+ let chainIds = crossChainState.chainIds;
153
+ let actionByChain = {};
154
+ let candidates = [];
155
+ for (let i = 0, i_finish = chainIds.length; i < i_finish; ++i) {
156
+ let chainId = chainIds[i];
157
+ let cs = crossChainState.chainStates[chainId];
158
+ let fetchState = ChainState.fetchState(cs);
159
+ let action = FetchState.getNextQuery(fetchState, remaining, ChainState.pendingBudget(cs));
160
+ if (typeof action !== "object") {
161
+ actionByChain[chainId] = action;
162
+ } else {
163
+ actionByChain[chainId] = "NothingToQuery";
164
+ action._0.forEach(query => {
165
+ query.chainId = chainId;
166
+ query.progress = FetchState.getProgressPercentageAt(fetchState, query.fromBlock);
167
+ candidates.push(query);
168
+ });
169
+ }
170
+ }
171
+ candidates.sort(compareByProgress);
172
+ let admittedByChain = {};
173
+ let running = 0;
174
+ let idx = 0;
175
+ while (running < remaining && idx < candidates.length) {
176
+ let query = candidates[idx];
177
+ Utils.Dict.push(admittedByChain, query.chainId.toString(), query);
178
+ running = running + query.estResponseSize;
179
+ idx = idx + 1 | 0;
180
+ };
181
+ Stdlib_Dict.forEachWithKey(admittedByChain, (queries, chainId) => {
182
+ actionByChain[chainId] = {
183
+ TAG: "Ready",
184
+ _0: queries
185
+ };
186
+ let chainId$1 = Stdlib_Int.fromString(chainId, undefined);
187
+ ChainState.startFetchingQueries(crossChainState.chainStates[chainId$1], queries);
188
+ });
167
189
  let promises = [];
168
- for (let i = 0, i_finish = priorityOrdered.length; i < i_finish; ++i) {
169
- let cs = priorityOrdered[i];
170
- let chain = ChainMap.Chain.makeUnsafe(ChainState.chainConfig(cs).id);
171
- let sourceManager = ChainState.sourceManager(cs);
172
- let concurrencyLimit = Primitive_int.max(0, maxConcurrency$1 - inFlight$1 | 0);
173
- let bufferLimit = crossChainState.targetBufferSize - (totalReady - FetchState.bufferSize(ChainState.fetchState(cs)) | 0) | 0;
174
- let inFlightBefore = SourceManager.inFlightCount(sourceManager);
175
- promises.push(fetchChain(chain, concurrencyLimit, bufferLimit));
176
- inFlight$1 = inFlight$1 + (SourceManager.inFlightCount(sourceManager) - inFlightBefore | 0) | 0;
190
+ for (let i$1 = 0, i_finish$1 = chainIds.length; i$1 < i_finish$1; ++i$1) {
191
+ let chainId$1 = chainIds[i$1];
192
+ let action$1 = actionByChain[chainId$1];
193
+ if (action$1 !== undefined) {
194
+ let exit = 0;
195
+ if (typeof action$1 === "object" || action$1 === "WaitingForNewBlock") {
196
+ exit = 1;
197
+ }
198
+ if (exit === 1) {
199
+ let chain = ChainMap.Chain.makeUnsafe(chainId$1);
200
+ promises.push(dispatchChain(chain, action$1));
201
+ }
202
+ }
177
203
  }
178
204
  await Promise.all(promises);
179
205
  }
@@ -184,8 +210,6 @@ export {
184
210
  chainStates,
185
211
  isRealtime,
186
212
  isInReorgThreshold,
187
- maxConcurrency,
188
- inFlight,
189
213
  nextItemIsNone,
190
214
  getSafeCheckpointId,
191
215
  createBatch,
@@ -9,8 +9,6 @@ let make: (
9
9
  ~chainStates: dict<ChainState.t>,
10
10
  ~isInReorgThreshold: bool,
11
11
  ~isRealtime: bool,
12
- ~maxBackfillConcurrency: int=?,
13
- ~maxRealtimeConcurrency: int=?,
14
12
  ~targetBufferSize: int=?,
15
13
  ) => t
16
14
 
@@ -18,8 +16,6 @@ let make: (
18
16
  let chainStates: t => dict<ChainState.t>
19
17
  let isRealtime: t => bool
20
18
  let isInReorgThreshold: t => bool
21
- let maxConcurrency: t => int
22
- let inFlight: t => int
23
19
 
24
20
  // Derived (pure).
25
21
  let nextItemIsNone: t => bool
@@ -39,9 +35,5 @@ let applyBatchProgress: (t, ~batch: Batch.t) => unit
39
35
  let priorityOrder: t => array<ChainState.t>
40
36
  let checkAndFetch: (
41
37
  t,
42
- ~fetchChain: (
43
- ~chain: ChainMap.Chain.t,
44
- ~concurrencyLimit: int,
45
- ~bufferLimit: int,
46
- ) => promise<unit>,
38
+ ~dispatchChain: (~chain: ChainMap.Chain.t, ~action: FetchState.nextQuery) => promise<unit>,
47
39
  ) => promise<unit>
package/src/Env.res CHANGED
@@ -8,13 +8,6 @@ let updateSyncTimeOnRestart =
8
8
  envSafe->EnvSafe.get("UPDATE_SYNC_TIME_ON_RESTART", S.bool, ~fallback=true)
9
9
  let targetBufferSize = envSafe->EnvSafe.get("ENVIO_INDEXING_MAX_BUFFER_SIZE", S.option(S.int))
10
10
  let maxAddrInPartition = envSafe->EnvSafe.get("MAX_PARTITION_SIZE", S.int, ~fallback=5_000)
11
- // Global caps on concurrent data-source queries across the whole indexer (all
12
- // chains share the budget), not per chain. Realtime allows far more since head
13
- // queries are small and many chains follow the head at once.
14
- let maxBackfillConcurrency =
15
- envSafe->EnvSafe.get("ENVIO_MAX_BACKFILL_CONCURRENCY", S.int, ~fallback=30)
16
- let maxRealtimeConcurrency =
17
- envSafe->EnvSafe.get("ENVIO_MAX_REALTIME_CONCURRENCY", S.int, ~fallback=200)
18
11
 
19
12
  // Target number of in-memory objects (uncommitted entity/effect changes plus
20
13
  // unwritten batch items) the store holds before processing waits for the write