envio 3.9.0 → 3.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/evm.schema.json +26 -0
- package/fuel.schema.json +26 -0
- package/index.d.ts +35 -3
- package/package.json +6 -7
- package/src/Batch.res +9 -0
- package/src/ChainState.res +35 -39
- package/src/ChainState.res.mjs +14 -11
- package/src/ChainState.resi +15 -7
- package/src/Config.res +52 -44
- package/src/Config.res.mjs +51 -17
- package/src/Core.res +4 -0
- package/src/CrossChainState.res +15 -23
- package/src/CrossChainState.res.mjs +10 -18
- package/src/CrossChainState.resi +4 -1
- package/src/Ecosystem.res +7 -2
- package/src/Envio.res +6 -4
- package/src/EventConfigBuilder.res +169 -26
- package/src/EventConfigBuilder.res.mjs +124 -15
- package/src/HandlerRegister.res +21 -4
- package/src/HandlerRegister.res.mjs +24 -5
- package/src/InMemoryStore.res +9 -7
- package/src/InMemoryStore.res.mjs +3 -3
- package/src/IndexerState.res +43 -6
- package/src/IndexerState.res.mjs +36 -7
- package/src/IndexerState.resi +4 -4
- package/src/Internal.res +16 -17
- package/src/Metrics.res +72 -6
- package/src/Metrics.res.mjs +30 -2
- package/src/Persistence.res +36 -30
- package/src/Persistence.res.mjs +1 -20
- package/src/PgStorage.res +104 -56
- package/src/PgStorage.res.mjs +57 -35
- package/src/PruneStaleHistory.res +85 -52
- package/src/PruneStaleHistory.res.mjs +63 -37
- package/src/Rollback.res +68 -45
- package/src/Rollback.res.mjs +38 -19
- package/src/RollbackCommit.res +7 -9
- package/src/RollbackCommit.res.mjs +5 -6
- package/src/SafeCheckpointTracking.res +11 -11
- package/src/SafeCheckpointTracking.res.mjs +7 -6
- package/src/SimulateItems.res +33 -18
- package/src/SimulateItems.res.mjs +26 -22
- package/src/Sink.res +38 -26
- package/src/Sink.res.mjs +21 -16
- package/src/TestIndexer.res +5 -5
- package/src/TestIndexer.res.mjs +1 -1
- package/src/Utils.res +170 -0
- package/src/Utils.res.mjs +153 -0
- package/src/Writing.res +2 -2
- package/src/Writing.res.mjs +1 -1
- package/src/bindings/ClickHouse.res +366 -698
- package/src/bindings/ClickHouse.res.mjs +371 -421
- package/src/bindings/ClickHouseSink.res +337 -0
- package/src/bindings/ClickHouseSink.res.mjs +246 -0
- package/src/bindings/EventSource.res +8 -2
- package/src/bindings/NodeJs.res +6 -0
- package/src/bindings/Vitest.res +23 -0
- package/src/bindings/Vitest.res.mjs +3 -0
- package/src/bindings/WebSocket.res +9 -10
- package/src/db/CheckpointBounds.res +53 -0
- package/src/db/CheckpointBounds.res.mjs +44 -0
- package/src/db/EntityFilter.res +36 -17
- package/src/db/EntityFilter.res.mjs +25 -14
- package/src/db/EntityHistory.res +47 -22
- package/src/db/EntityHistory.res.mjs +19 -12
- package/src/db/InternalTable.res +84 -35
- package/src/db/InternalTable.res.mjs +56 -23
- package/src/db/RollbackFloors.res +55 -0
- package/src/db/RollbackFloors.res.mjs +92 -0
- package/src/db/Table.res +22 -4
- package/src/db/Table.res.mjs +30 -9
- package/src/sources/Evm.res +2 -0
- package/src/sources/Evm.res.mjs +2 -0
- package/src/sources/EvmHyperSyncSource.res +3 -3
- package/src/sources/EvmHyperSyncSource.res.mjs +3 -3
- package/src/sources/EvmRpcWs.res +124 -0
- package/src/sources/EvmRpcWs.res.mjs +117 -0
- package/src/sources/Fuel.res +2 -0
- package/src/sources/Fuel.res.mjs +2 -0
- package/src/sources/FuelHyperSyncSource.res +1 -1
- package/src/sources/FuelHyperSyncSource.res.mjs +2 -1
- package/src/sources/HeightFeed.res +568 -0
- package/src/sources/HeightFeed.res.mjs +452 -0
- package/src/sources/HeightStream.res +257 -0
- package/src/sources/HeightStream.res.mjs +194 -0
- package/src/sources/HyperSync.res +5 -0
- package/src/sources/HyperSync.res.mjs +3 -0
- package/src/sources/HyperSync.resi +4 -0
- package/src/sources/HyperSyncSSE.res +49 -0
- package/src/sources/HyperSyncSSE.res.mjs +79 -0
- package/src/sources/RpcSource.res +1 -1
- package/src/sources/RpcSource.res.mjs +2 -2
- package/src/sources/Source.res +54 -1
- package/src/sources/Source.res.mjs +25 -0
- package/src/sources/SourceManager.res +220 -227
- package/src/sources/SourceManager.res.mjs +139 -164
- package/src/sources/SourceManager.resi +8 -0
- package/src/sources/Svm.res +2 -0
- package/src/sources/Svm.res.mjs +2 -0
- package/src/sources/SvmHyperSyncClient.res +70 -127
- package/src/sources/SvmHyperSyncClient.res.mjs +51 -25
- package/src/sources/SvmHyperSyncSource.res +18 -15
- package/src/sources/SvmHyperSyncSource.res.mjs +5 -4
- package/svm.schema.json +1 -75
- package/src/MemoryStorage.res +0 -729
- package/src/MemoryStorage.res.mjs +0 -586
- package/src/sources/HyperSyncHeightStream.res +0 -129
- package/src/sources/HyperSyncHeightStream.res.mjs +0 -110
- package/src/sources/RpcWebSocketHeightStream.res +0 -175
- package/src/sources/RpcWebSocketHeightStream.res.mjs +0 -180
|
@@ -7,9 +7,7 @@ type requestStatAgg = {mutable count: int, mutable seconds: float}
|
|
|
7
7
|
|
|
8
8
|
type sourceState = {
|
|
9
9
|
source: Source.t,
|
|
10
|
-
|
|
11
|
-
mutable unsubscribe: option<unit => unit>,
|
|
12
|
-
mutable pendingHeightResolvers: array<int => unit>,
|
|
10
|
+
feed: HeightFeed.t,
|
|
13
11
|
mutable disabled: bool,
|
|
14
12
|
// Timestamp (ms) when this source last failed during executeQuery.
|
|
15
13
|
// Used to decide when to attempt recovery to this source.
|
|
@@ -17,17 +15,23 @@ type sourceState = {
|
|
|
17
15
|
requestStats: dict<requestStatAgg>,
|
|
18
16
|
}
|
|
19
17
|
|
|
20
|
-
let
|
|
18
|
+
let recordStatsInto = (
|
|
19
|
+
aggregates: dict<requestStatAgg>,
|
|
20
|
+
requestStats: array<Source.requestStat>,
|
|
21
|
+
) => {
|
|
21
22
|
requestStats->Array.forEach(({method, seconds}) => {
|
|
22
|
-
switch
|
|
23
|
+
switch aggregates->Utils.Dict.dangerouslyGetNonOption(method) {
|
|
23
24
|
| Some(agg) =>
|
|
24
25
|
agg.count = agg.count + 1
|
|
25
26
|
agg.seconds = agg.seconds +. seconds
|
|
26
|
-
| None =>
|
|
27
|
+
| None => aggregates->Dict.set(method, {count: 1, seconds})
|
|
27
28
|
}
|
|
28
29
|
})
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
let recordRequestStats = (sourceState: sourceState, requestStats: array<Source.requestStat>) =>
|
|
33
|
+
sourceState.requestStats->recordStatsInto(requestStats)
|
|
34
|
+
|
|
31
35
|
// Flattened (source, method) aggregates for Metrics.renderSourceRequests to
|
|
32
36
|
// inline into the /metrics response.
|
|
33
37
|
type requestStatSample = {
|
|
@@ -53,7 +57,6 @@ type t = {
|
|
|
53
57
|
newBlockStallTimeoutRealtime: int,
|
|
54
58
|
stalledPollingInterval: int,
|
|
55
59
|
reducedPollingInterval: int,
|
|
56
|
-
getHeightRetryInterval: (~retry: int) => int,
|
|
57
60
|
mutable activeSource: Source.t,
|
|
58
61
|
mutable waitingForNewBlockStateId: option<int>,
|
|
59
62
|
// Dedupes the "waiting for new blocks" trace so it fires once per contiguous
|
|
@@ -108,17 +111,45 @@ type sourceHeightSample = {
|
|
|
108
111
|
let getSourceHeightSamples = (sourceManager: t): array<sourceHeightSample> => {
|
|
109
112
|
let samples = []
|
|
110
113
|
sourceManager.sourcesState->Array.forEach(sourceState => {
|
|
111
|
-
|
|
114
|
+
let knownHeight = sourceState.feed->HeightFeed.knownHeight
|
|
115
|
+
if knownHeight > 0 {
|
|
112
116
|
samples->Array.push({
|
|
113
117
|
sourceName: sourceState.source.name,
|
|
114
118
|
chainId: sourceState.source.chainId,
|
|
115
|
-
height:
|
|
119
|
+
height: knownHeight,
|
|
116
120
|
})
|
|
117
121
|
}
|
|
118
122
|
})
|
|
119
123
|
samples
|
|
120
124
|
}
|
|
121
125
|
|
|
126
|
+
// Per-source height subscription health for envio_source_height_stream_*.
|
|
127
|
+
// Every source a wait has asked for a stream is reported, including one whose
|
|
128
|
+
// stream has never come up — that is a stream nothing else would say anything
|
|
129
|
+
// about. A source that was never asked is skipped, so a chain that only ever
|
|
130
|
+
// polls renders none of it rather than sitting at zero on it.
|
|
131
|
+
type heightStreamSample = {
|
|
132
|
+
sourceName: string,
|
|
133
|
+
chainId: ChainId.t,
|
|
134
|
+
stream: HeightFeed.streamSample,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let getHeightStreamSamples = (sourceManager: t): array<heightStreamSample> => {
|
|
138
|
+
let samples = []
|
|
139
|
+
sourceManager.sourcesState->Array.forEach(sourceState =>
|
|
140
|
+
switch sourceState.feed->HeightFeed.sample {
|
|
141
|
+
| Some(stream) =>
|
|
142
|
+
samples->Array.push({
|
|
143
|
+
sourceName: sourceState.source.name,
|
|
144
|
+
chainId: sourceState.source.chainId,
|
|
145
|
+
stream,
|
|
146
|
+
})
|
|
147
|
+
| None => ()
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
samples
|
|
151
|
+
}
|
|
152
|
+
|
|
122
153
|
// Each accessor adds the in-progress interval of the current status, so a
|
|
123
154
|
// scrape during a long idle/wait/query isn't stale until the next transition.
|
|
124
155
|
let idleSeconds = (sourceManager: t) =>
|
|
@@ -308,13 +339,18 @@ let make = (
|
|
|
308
339
|
}
|
|
309
340
|
{
|
|
310
341
|
sourcesState: sources->Array.map(source => {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
342
|
+
let requestStats = Dict.make()
|
|
343
|
+
{
|
|
344
|
+
source,
|
|
345
|
+
feed: HeightFeed.make(
|
|
346
|
+
~source,
|
|
347
|
+
~recordRequestStats=stats => requestStats->recordStatsInto(stats),
|
|
348
|
+
~getHeightRetryInterval,
|
|
349
|
+
),
|
|
350
|
+
disabled: false,
|
|
351
|
+
lastFailedAt: None,
|
|
352
|
+
requestStats,
|
|
353
|
+
}
|
|
318
354
|
}),
|
|
319
355
|
activeSource: initialActiveSource,
|
|
320
356
|
waitingForNewBlockStateId: None,
|
|
@@ -324,7 +360,6 @@ let make = (
|
|
|
324
360
|
newBlockStallTimeoutRealtime,
|
|
325
361
|
stalledPollingInterval,
|
|
326
362
|
reducedPollingInterval,
|
|
327
|
-
getHeightRetryInterval,
|
|
328
363
|
recoveryTimeout,
|
|
329
364
|
statusStart: Performance.now(),
|
|
330
365
|
status: Idle,
|
|
@@ -406,15 +441,10 @@ let dispatch = async (
|
|
|
406
441
|
}
|
|
407
442
|
}
|
|
408
443
|
|
|
409
|
-
type status = Active | Stalled | Done
|
|
410
|
-
|
|
411
444
|
let disableSource = (sourceManager: t, sourceState: sourceState) => {
|
|
412
445
|
if !sourceState.disabled {
|
|
413
446
|
sourceState.disabled = true
|
|
414
|
-
|
|
415
|
-
| Some(unsubscribe) => unsubscribe()
|
|
416
|
-
| None => ()
|
|
417
|
-
}
|
|
447
|
+
sourceState.feed->HeightFeed.stop
|
|
418
448
|
if sourceState.source.sourceFor === Realtime {
|
|
419
449
|
// Only clear hasRealtime if no other non-disabled Realtime sources remain
|
|
420
450
|
let hasOtherRealtime =
|
|
@@ -429,123 +459,6 @@ let disableSource = (sourceManager: t, sourceState: sourceState) => {
|
|
|
429
459
|
}
|
|
430
460
|
}
|
|
431
461
|
|
|
432
|
-
let getSourceNewHeight = async (
|
|
433
|
-
sourceManager,
|
|
434
|
-
~sourceState: sourceState,
|
|
435
|
-
~knownHeight,
|
|
436
|
-
~stallTimeout,
|
|
437
|
-
~isRealtime,
|
|
438
|
-
~status: ref<status>,
|
|
439
|
-
~logger,
|
|
440
|
-
~reducedPolling,
|
|
441
|
-
) => {
|
|
442
|
-
let source = sourceState.source
|
|
443
|
-
let initialHeight = sourceState.knownHeight
|
|
444
|
-
let newHeight = ref(initialHeight)
|
|
445
|
-
let retry = ref(0)
|
|
446
|
-
|
|
447
|
-
while newHeight.contents <= knownHeight && status.contents !== Done {
|
|
448
|
-
switch sourceState.unsubscribe {
|
|
449
|
-
| Some(_) =>
|
|
450
|
-
let subscriptionPromise = Promise.make((resolve, _reject) => {
|
|
451
|
-
sourceState.pendingHeightResolvers->Array.push(resolve)
|
|
452
|
-
})
|
|
453
|
-
// If the subscription goes quiet for half the stall timeout, fall back to REST
|
|
454
|
-
// polling. Jitter the trigger across [stallTimeout/2, stallTimeout) so indexers
|
|
455
|
-
// that go quiet together don't all start polling at the same instant.
|
|
456
|
-
let half = stallTimeout / 2
|
|
457
|
-
let pollingFallback = Utils.delay(
|
|
458
|
-
half + (Math.random() *. half->Int.toFloat)->Float.toInt,
|
|
459
|
-
)->Promise.then(async () => {
|
|
460
|
-
logger->Logging.childTrace({
|
|
461
|
-
"msg": "onHeight subscription stale, switching to polling fallback",
|
|
462
|
-
"source": source.name,
|
|
463
|
-
"chainId": source.chainId,
|
|
464
|
-
})
|
|
465
|
-
let h = ref(initialHeight)
|
|
466
|
-
while h.contents <= knownHeight && !(newHeight.contents > initialHeight) {
|
|
467
|
-
try {
|
|
468
|
-
let res = await source.getHeightOrThrow()
|
|
469
|
-
sourceState->recordRequestStats(res.requestStats)
|
|
470
|
-
h := res.height
|
|
471
|
-
} catch {
|
|
472
|
-
| _ => ()
|
|
473
|
-
}
|
|
474
|
-
if h.contents <= knownHeight && !(newHeight.contents > initialHeight) {
|
|
475
|
-
await Utils.delay(source.pollingInterval)
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
h.contents
|
|
479
|
-
})
|
|
480
|
-
let height = await Promise.race([subscriptionPromise, pollingFallback])
|
|
481
|
-
|
|
482
|
-
// Only accept heights greater than initialHeight
|
|
483
|
-
if height > initialHeight {
|
|
484
|
-
newHeight := height
|
|
485
|
-
}
|
|
486
|
-
| None =>
|
|
487
|
-
// No subscription, use REST polling
|
|
488
|
-
try {
|
|
489
|
-
let res = await source.getHeightOrThrow()
|
|
490
|
-
sourceState->recordRequestStats(res.requestStats)
|
|
491
|
-
let height = res.height
|
|
492
|
-
|
|
493
|
-
newHeight := height
|
|
494
|
-
if height <= knownHeight {
|
|
495
|
-
retry := 0
|
|
496
|
-
|
|
497
|
-
// If createHeightSubscription is available and height hasn't changed,
|
|
498
|
-
// create subscription instead of polling
|
|
499
|
-
switch source.createHeightSubscription {
|
|
500
|
-
| Some(createSubscription) if isRealtime =>
|
|
501
|
-
let unsubscribe = createSubscription(~onHeight=newHeight => {
|
|
502
|
-
// Ignore non-increasing heights. The height stream re-emits the current
|
|
503
|
-
// head on every (re)connect; waking the wait loop on a height we already
|
|
504
|
-
// know spins it and leaks fallback pollers (#1270).
|
|
505
|
-
if newHeight > sourceState.knownHeight {
|
|
506
|
-
sourceState->recordRequestStats([{Source.method: "heightPush", seconds: 0.}])
|
|
507
|
-
sourceState.knownHeight = newHeight
|
|
508
|
-
let resolvers = sourceState.pendingHeightResolvers
|
|
509
|
-
sourceState.pendingHeightResolvers = []
|
|
510
|
-
resolvers->Array.forEach(resolve => resolve(newHeight))
|
|
511
|
-
} else {
|
|
512
|
-
sourceState->recordRequestStats([{Source.method: "heightPushIgnored", seconds: 0.}])
|
|
513
|
-
}
|
|
514
|
-
})
|
|
515
|
-
sourceState.unsubscribe = Some(unsubscribe)
|
|
516
|
-
| _ =>
|
|
517
|
-
// Slowdown polling when the chain isn't progressing
|
|
518
|
-
let pollingInterval = if reducedPolling {
|
|
519
|
-
sourceManager.reducedPollingInterval
|
|
520
|
-
} else if status.contents === Stalled {
|
|
521
|
-
sourceManager.stalledPollingInterval
|
|
522
|
-
} else {
|
|
523
|
-
source.pollingInterval
|
|
524
|
-
}
|
|
525
|
-
await Utils.delay(pollingInterval)
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
} catch {
|
|
529
|
-
| exn =>
|
|
530
|
-
let retryInterval = sourceManager.getHeightRetryInterval(~retry=retry.contents)
|
|
531
|
-
logger->Logging.childTrace({
|
|
532
|
-
"msg": `Height retrieval from ${source.name} source failed. Retrying in ${retryInterval->Int.toString}ms.`,
|
|
533
|
-
"source": source.name,
|
|
534
|
-
"err": exn->Utils.prettifyExn,
|
|
535
|
-
})
|
|
536
|
-
retry := retry.contents + 1
|
|
537
|
-
await Utils.delay(retryInterval)
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
if newHeight.contents > sourceState.knownHeight {
|
|
543
|
-
sourceState.knownHeight = newHeight.contents
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
newHeight.contents
|
|
547
|
-
}
|
|
548
|
-
|
|
549
462
|
let compareByOldestFailure = (a: sourceState, b: sourceState) =>
|
|
550
463
|
switch (a.lastFailedAt, b.lastFailedAt) {
|
|
551
464
|
| (None, Some(_)) => Ordering.less
|
|
@@ -620,7 +533,7 @@ let maxRetryBackoffMillis = 60_000
|
|
|
620
533
|
// 100ms doubling up to the cap. `backoffBeforeRetry` still applies the caller's
|
|
621
534
|
// floor and the cap on top.
|
|
622
535
|
let retryBackoffMillis = retry =>
|
|
623
|
-
|
|
536
|
+
Utils.expBackoff(~base=100, ~exp=retry, ~maxMillis=maxRetryBackoffMillis)
|
|
624
537
|
|
|
625
538
|
// Floor for the retries driven by a condition the source reported itself
|
|
626
539
|
// (behind the head, inconsistent response). Unlike a caller-supplied backoff of
|
|
@@ -754,7 +667,12 @@ let retryInconsistentResponse = async (
|
|
|
754
667
|
}
|
|
755
668
|
|
|
756
669
|
// Polls for a block height greater than the given block number to ensure a new block is available for indexing.
|
|
757
|
-
|
|
670
|
+
/*
|
|
671
|
+
Resolves at the first height above `knownHeight` that any eligible source
|
|
672
|
+
reports. A waiter is registered per source and the first one to fire wins,
|
|
673
|
+
cancelling the rest.
|
|
674
|
+
*/
|
|
675
|
+
let waitForNewBlock = (sourceManager: t, ~knownHeight, ~isRealtime, ~reducedPolling) => {
|
|
758
676
|
let {sourcesState} = sourceManager
|
|
759
677
|
|
|
760
678
|
let logger = Logging.createChild(
|
|
@@ -775,7 +693,9 @@ let waitForNewBlock = async (sourceManager: t, ~knownHeight, ~isRealtime, ~reduc
|
|
|
775
693
|
|
|
776
694
|
let mainSources = sourceManager->getNextSources(~isRealtime)
|
|
777
695
|
|
|
778
|
-
|
|
696
|
+
// Whether this wait has already run out its stall window. It only changes the
|
|
697
|
+
// cadence the sources poll at and the level the closing line is logged at.
|
|
698
|
+
let stalled = ref(false)
|
|
779
699
|
|
|
780
700
|
// Use a much longer stall timeout when reduced polling is active
|
|
781
701
|
// to avoid spurious stall warnings while waiting for other chains to backfill
|
|
@@ -787,94 +707,168 @@ let waitForNewBlock = async (sourceManager: t, ~knownHeight, ~isRealtime, ~reduc
|
|
|
787
707
|
sourceManager.newBlockStallTimeout
|
|
788
708
|
}
|
|
789
709
|
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
) {
|
|
820
|
-
fallbackSources->Array.push(sourceState)
|
|
821
|
-
}
|
|
710
|
+
Promise.make((resolve, _reject) => {
|
|
711
|
+
// Every waiter this wait holds, on primaries and on any fallback recruited
|
|
712
|
+
// later, so a stall reaches all of them.
|
|
713
|
+
let watched: array<HeightFeed.subscription> = []
|
|
714
|
+
let pokeTimeoutId = ref(None)
|
|
715
|
+
let stallTimeoutId = ref(None)
|
|
716
|
+
|
|
717
|
+
// Safe to repeat: unsubscribing twice removes a waiter that is already gone.
|
|
718
|
+
let cleanup = () => {
|
|
719
|
+
watched->Array.forEach(subscription => subscription.unsubscribe())
|
|
720
|
+
watched->Utils.Array.clearInPlace
|
|
721
|
+
pokeTimeoutId->Utils.clearTimeoutRef
|
|
722
|
+
stallTimeoutId->Utils.clearTimeoutRef
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
let settled = ref(false)
|
|
726
|
+
let settle = (source: Source.t, height) =>
|
|
727
|
+
if !settled.contents {
|
|
728
|
+
settled := true
|
|
729
|
+
// Before anything else: the sources still watching have no reason to
|
|
730
|
+
// keep polling for a height this wait already has.
|
|
731
|
+
cleanup()
|
|
732
|
+
sourceManager.activeSource = source
|
|
733
|
+
// Show a higher level log if we displayed a warning/error after newBlockStallTimeout
|
|
734
|
+
let log = stalled.contents ? Logging.childInfo : Logging.childTrace
|
|
735
|
+
logger->log({
|
|
736
|
+
"msg": `New blocks successfully found.`,
|
|
737
|
+
"source": source.name,
|
|
738
|
+
"newBlockHeight": height,
|
|
822
739
|
})
|
|
740
|
+
sourceManager.waitingLogged = false
|
|
741
|
+
resolve(height)
|
|
742
|
+
}
|
|
823
743
|
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
744
|
+
// Registering can answer the wait on the spot, from a height the source
|
|
745
|
+
// already knew, so every site has to cope with the wait being over — either
|
|
746
|
+
// before it got here, or because of its own call. Handled once here rather
|
|
747
|
+
// than at each place that registers.
|
|
748
|
+
let watch = (sourceState: sourceState, ~withStream) =>
|
|
749
|
+
if !settled.contents {
|
|
750
|
+
if withStream {
|
|
751
|
+
// Lazy and explicit: a source that can push heights subscribes when a
|
|
752
|
+
// realtime wait starts wanting them, and not before. A stream outlives
|
|
753
|
+
// the wait that asked for it and nothing takes it back off, so only the
|
|
754
|
+
// sources this wait was built on ask for one — a source recruited
|
|
755
|
+
// because another stalled is here to poll, and would otherwise hold a
|
|
756
|
+
// connection open for the life of the process on the strength of one
|
|
757
|
+
// stall.
|
|
758
|
+
sourceState.feed->HeightFeed.enableStream
|
|
839
759
|
}
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
~reducedPolling,
|
|
854
|
-
),
|
|
855
|
-
)
|
|
856
|
-
}),
|
|
760
|
+
let subscription = sourceState.feed->HeightFeed.onHeightAbove(
|
|
761
|
+
~knownHeight,
|
|
762
|
+
// Read per poll rather than captured, so a wait that goes on to stall
|
|
763
|
+
// slows its own polling down without anything having to restart it.
|
|
764
|
+
~interval=() =>
|
|
765
|
+
if reducedPolling {
|
|
766
|
+
sourceManager.reducedPollingInterval
|
|
767
|
+
} else if stalled.contents {
|
|
768
|
+
sourceManager.stalledPollingInterval
|
|
769
|
+
} else {
|
|
770
|
+
sourceState.source.pollingInterval
|
|
771
|
+
},
|
|
772
|
+
~onHeight=height => settle(sourceState.source, height),
|
|
857
773
|
)
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
774
|
+
if settled.contents {
|
|
775
|
+
// This registration is what answered the wait, and the cleanup that ran
|
|
776
|
+
// inside it could not reach a subscription it had not returned yet.
|
|
777
|
+
subscription.unsubscribe()
|
|
778
|
+
} else {
|
|
779
|
+
watched->Array.push(subscription)->ignore
|
|
780
|
+
}
|
|
781
|
+
}
|
|
863
782
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
783
|
+
mainSources->Array.forEach(sourceState => sourceState->watch(~withStream=isRealtime))
|
|
784
|
+
|
|
785
|
+
if !settled.contents {
|
|
786
|
+
// Spread across the window, and re-spread on every repeat: every indexer on
|
|
787
|
+
// one provider goes quiet in the same instant that provider does, and
|
|
788
|
+
// putting them all on one schedule is how a quiet chain becomes a stampede.
|
|
789
|
+
// Re-armed because distrusting a stream is a one-shot — the next height it
|
|
790
|
+
// delivers takes it back at its word — and the silence that earned it can
|
|
791
|
+
// come straight back.
|
|
792
|
+
let rec armPokeTimeout = () =>
|
|
793
|
+
pokeTimeoutId := Some(setTimeout(() => {
|
|
794
|
+
pokeTimeoutId := None
|
|
795
|
+
if !settled.contents {
|
|
796
|
+
watched->Array.forEach(subscription => subscription.distrustStream())
|
|
797
|
+
}
|
|
872
798
|
|
|
873
|
-
|
|
799
|
+
// Re-checked: distrusting can answer the wait, and a timer armed
|
|
800
|
+
// after the cleanup that follows is one nothing can ever clear.
|
|
801
|
+
if !settled.contents {
|
|
802
|
+
armPokeTimeout()
|
|
803
|
+
}
|
|
804
|
+
}, Utils.jitter(stallTimeout)))
|
|
805
|
+
|
|
806
|
+
// Punctual, unlike the poke above: newBlockStallTimeout is a promise to the
|
|
807
|
+
// operator about when they hear about a quiet chain, and spreading that
|
|
808
|
+
// would report it early. Fires once — the warning is worth saying once per
|
|
809
|
+
// wait, and a fallback stays recruited.
|
|
810
|
+
let armStallTimeout = () =>
|
|
811
|
+
stallTimeoutId := Some(setTimeout(() => {
|
|
812
|
+
stallTimeoutId := None
|
|
813
|
+
if !settled.contents {
|
|
814
|
+
stalled := true
|
|
815
|
+
|
|
816
|
+
// Build fallback: non-disabled sources not in mainSources with a valid role, even with recent lastFailedAt
|
|
817
|
+
let fallbackSources = []
|
|
818
|
+
sourcesState->Array.forEach(
|
|
819
|
+
sourceState => {
|
|
820
|
+
if (
|
|
821
|
+
!sourceState.disabled &&
|
|
822
|
+
!(mainSources->Array.includes(sourceState)) &&
|
|
823
|
+
getSourceRole(
|
|
824
|
+
~sourceFor=sourceState.source.sourceFor,
|
|
825
|
+
~isRealtime,
|
|
826
|
+
~hasRealtime=sourceManager.hasRealtime,
|
|
827
|
+
)->Option.isSome
|
|
828
|
+
) {
|
|
829
|
+
fallbackSources->Array.push(sourceState)
|
|
830
|
+
}
|
|
831
|
+
},
|
|
832
|
+
)
|
|
833
|
+
|
|
834
|
+
switch fallbackSources {
|
|
835
|
+
| [] =>
|
|
836
|
+
logger->Logging.childWarn(
|
|
837
|
+
`No new blocks detected within ${(stallTimeout / 1000)
|
|
838
|
+
->Int.toString}s. Polling will continue at a reduced rate. For better reliability, refer to our RPC fallback guide: https://docs.envio.dev/docs/HyperIndex/rpc-sync`,
|
|
839
|
+
)
|
|
840
|
+
| _ =>
|
|
841
|
+
logger->Logging.childWarn(
|
|
842
|
+
`No new blocks detected within ${(stallTimeout / 1000)
|
|
843
|
+
->Int.toString}s. Continuing polling with secondary RPC sources from the configuration.`,
|
|
844
|
+
)
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// Recruited to poll, not to stream: see `watch`.
|
|
848
|
+
fallbackSources->Array.forEach(
|
|
849
|
+
sourceState => sourceState->watch(~withStream=false),
|
|
850
|
+
)
|
|
851
|
+
|
|
852
|
+
// A fallback recruited here can still be holding a stream from an
|
|
853
|
+
// earlier wait that made it a primary, and a live stream is not
|
|
854
|
+
// polled behind. It has to inherit the verdict the primaries
|
|
855
|
+
// already earned — this wait has heard nothing for a whole window
|
|
856
|
+
// — or it would sit silent behind that stream until the next
|
|
857
|
+
// spread poke. Distrusting a waiter that already does so is a
|
|
858
|
+
// no-op, so this reaches the new ones only.
|
|
859
|
+
if !settled.contents {
|
|
860
|
+
watched->Array.forEach(subscription => subscription.distrustStream())
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}, stallTimeout))
|
|
874
864
|
|
|
875
|
-
|
|
865
|
+
armPokeTimeout()
|
|
866
|
+
armStallTimeout()
|
|
867
|
+
}
|
|
868
|
+
})
|
|
876
869
|
}
|
|
877
870
|
|
|
871
|
+
|
|
878
872
|
let executeQuery = async (
|
|
879
873
|
sourceManager: t,
|
|
880
874
|
~query: FetchState.query,
|
|
@@ -946,11 +940,10 @@ let executeQuery = async (
|
|
|
946
940
|
sourceState.lastFailedAt = None
|
|
947
941
|
|
|
948
942
|
// The response carries a fresh height for exactly this source, so during a
|
|
949
|
-
// long backfill (when
|
|
950
|
-
// envio_source_known_height current
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
}
|
|
943
|
+
// long backfill (when nothing is waiting on the feed) it keeps the
|
|
944
|
+
// per-source envio_source_known_height current — and if a wait is in
|
|
945
|
+
// flight, a height learned this way can settle it.
|
|
946
|
+
sourceState.feed->HeightFeed.recordHeight(response.knownHeight)
|
|
954
947
|
responseRef := Some(response)
|
|
955
948
|
} catch {
|
|
956
949
|
| Source.RateLimited({resetMs, requestStats}) =>
|