envio 3.0.0-alpha.3 → 3.0.0-alpha.4
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/README.md +2 -2
- package/evm.schema.json +0 -1
- package/index.d.ts +4 -2
- package/index.js +1 -0
- package/package.json +5 -5
- package/src/Config.res +1 -1
- package/src/Config.res.mjs +4 -4
- package/src/Ecosystem.res +2 -2
- package/src/Ecosystem.res.mjs +1 -1
- package/src/Envio.gen.ts +1 -1
- package/src/Envio.res +1 -1
- package/src/Internal.res +41 -0
- package/src/Types.ts +1 -1
- package/src/Utils.res +15 -0
- package/src/Utils.res.mjs +18 -0
- package/src/bindings/ClickHouse.res +31 -1
- package/src/bindings/ClickHouse.res.mjs +27 -1
- package/src/bindings/Ethers.res +27 -63
- package/src/bindings/Ethers.res.mjs +18 -65
- package/src/sources/HyperSyncHeightStream.res +28 -110
- package/src/sources/HyperSyncHeightStream.res.mjs +30 -63
- package/src/sources/HyperSyncSource.res +11 -13
- package/src/sources/HyperSyncSource.res.mjs +20 -20
- package/src/sources/Rpc.res +43 -0
- package/src/sources/Rpc.res.mjs +31 -0
- package/src/sources/RpcSource.res +9 -4
- package/src/sources/RpcSource.res.mjs +9 -4
- package/src/sources/Source.res +1 -0
- package/src/sources/SourceManager.res +164 -81
- package/src/sources/SourceManager.res.mjs +146 -83
- package/src/sources/{Solana.res → Svm.res} +4 -4
- package/src/sources/{Solana.res.mjs → Svm.res.mjs} +4 -4
- package/src/bindings/Ethers.gen.ts +0 -14
|
@@ -2,13 +2,21 @@ open Belt
|
|
|
2
2
|
|
|
3
3
|
type sourceManagerStatus = Idle | WaitingForNewBlock | Querieng
|
|
4
4
|
|
|
5
|
+
type sourceState = {
|
|
6
|
+
source: Source.t,
|
|
7
|
+
mutable knownHeight: int,
|
|
8
|
+
mutable unsubscribe: option<unit => unit>,
|
|
9
|
+
mutable pendingHeightResolvers: array<int => unit>,
|
|
10
|
+
mutable disabled: bool,
|
|
11
|
+
}
|
|
12
|
+
|
|
5
13
|
// Ideally the ChainFetcher name suits this better
|
|
6
14
|
// But currently the ChainFetcher module is immutable
|
|
7
15
|
// and handles both processing and fetching.
|
|
8
16
|
// So this module is to encapsulate the fetching logic only
|
|
9
17
|
// with a mutable state for easier reasoning and testing.
|
|
10
18
|
type t = {
|
|
11
|
-
|
|
19
|
+
sourcesState: array<sourceState>,
|
|
12
20
|
mutable statusStart: Hrtime.timeRef,
|
|
13
21
|
mutable status: sourceManagerStatus,
|
|
14
22
|
maxPartitionConcurrency: int,
|
|
@@ -63,7 +71,13 @@ let make = (
|
|
|
63
71
|
)
|
|
64
72
|
{
|
|
65
73
|
maxPartitionConcurrency,
|
|
66
|
-
|
|
74
|
+
sourcesState: sources->Array.map(source => {
|
|
75
|
+
source,
|
|
76
|
+
knownHeight: 0,
|
|
77
|
+
unsubscribe: None,
|
|
78
|
+
pendingHeightResolvers: [],
|
|
79
|
+
disabled: false,
|
|
80
|
+
}),
|
|
67
81
|
activeSource: initialActiveSource,
|
|
68
82
|
waitingForNewBlockStateId: None,
|
|
69
83
|
fetchingPartitionsCount: 0,
|
|
@@ -159,60 +173,109 @@ let fetchNext = async (
|
|
|
159
173
|
|
|
160
174
|
type status = Active | Stalled | Done
|
|
161
175
|
|
|
176
|
+
let disableSource = (sourceState: sourceState) => {
|
|
177
|
+
if !sourceState.disabled {
|
|
178
|
+
sourceState.disabled = true
|
|
179
|
+
switch sourceState.unsubscribe {
|
|
180
|
+
| Some(unsubscribe) => unsubscribe()
|
|
181
|
+
| None => ()
|
|
182
|
+
}
|
|
183
|
+
true
|
|
184
|
+
} else {
|
|
185
|
+
false
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
162
189
|
let getSourceNewHeight = async (
|
|
163
190
|
sourceManager,
|
|
164
|
-
~
|
|
191
|
+
~sourceState: sourceState,
|
|
165
192
|
~knownHeight,
|
|
166
193
|
~status: ref<status>,
|
|
167
194
|
~logger,
|
|
168
195
|
) => {
|
|
169
|
-
let
|
|
196
|
+
let source = sourceState.source
|
|
197
|
+
let initialHeight = sourceState.knownHeight
|
|
198
|
+
let newHeight = ref(initialHeight)
|
|
170
199
|
let retry = ref(0)
|
|
171
200
|
|
|
172
201
|
while newHeight.contents <= knownHeight && status.contents !== Done {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
202
|
+
// If subscription exists, wait for next height event
|
|
203
|
+
switch sourceState.unsubscribe {
|
|
204
|
+
| Some(_) =>
|
|
205
|
+
let height = await Promise.make((resolve, _reject) => {
|
|
206
|
+
sourceState.pendingHeightResolvers->Array.push(resolve)
|
|
178
207
|
})
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
source.
|
|
208
|
+
|
|
209
|
+
// Only accept heights greater than initialHeight
|
|
210
|
+
if height > initialHeight {
|
|
211
|
+
newHeight := height
|
|
212
|
+
}
|
|
213
|
+
| None =>
|
|
214
|
+
// No subscription, use REST polling
|
|
215
|
+
try {
|
|
216
|
+
// Use to detect if the source is taking too long to respond
|
|
217
|
+
let endTimer = Prometheus.SourceGetHeightDuration.startTimer({
|
|
218
|
+
"source": source.name,
|
|
219
|
+
"chainId": source.chain->ChainMap.Chain.toChainId,
|
|
220
|
+
})
|
|
221
|
+
let height = await source.getHeightOrThrow()
|
|
222
|
+
endTimer()
|
|
223
|
+
|
|
224
|
+
newHeight := height
|
|
225
|
+
if height <= knownHeight {
|
|
226
|
+
retry := 0
|
|
227
|
+
|
|
228
|
+
// If createHeightSubscription is available and height hasn't changed,
|
|
229
|
+
// create subscription instead of polling
|
|
230
|
+
switch source.createHeightSubscription {
|
|
231
|
+
| Some(createSubscription) =>
|
|
232
|
+
let unsubscribe = createSubscription(~onHeight=newHeight => {
|
|
233
|
+
sourceState.knownHeight = newHeight
|
|
234
|
+
// Resolve all pending height resolvers
|
|
235
|
+
let resolvers = sourceState.pendingHeightResolvers
|
|
236
|
+
sourceState.pendingHeightResolvers = []
|
|
237
|
+
resolvers->Array.forEach(resolve => resolve(newHeight))
|
|
238
|
+
})
|
|
239
|
+
sourceState.unsubscribe = Some(unsubscribe)
|
|
240
|
+
| None =>
|
|
241
|
+
// Slowdown polling when the chain isn't progressing
|
|
242
|
+
let pollingInterval = if status.contents === Stalled {
|
|
243
|
+
sourceManager.stalledPollingInterval
|
|
244
|
+
} else {
|
|
245
|
+
source.pollingInterval
|
|
246
|
+
}
|
|
247
|
+
await Utils.delay(pollingInterval)
|
|
248
|
+
}
|
|
190
249
|
}
|
|
191
|
-
|
|
250
|
+
} catch {
|
|
251
|
+
| exn =>
|
|
252
|
+
let retryInterval = sourceManager.getHeightRetryInterval(~retry=retry.contents)
|
|
253
|
+
logger->Logging.childTrace({
|
|
254
|
+
"msg": `Height retrieval from ${source.name} source failed. Retrying in ${retryInterval->Int.toString}ms.`,
|
|
255
|
+
"source": source.name,
|
|
256
|
+
"err": exn->Utils.prettifyExn,
|
|
257
|
+
})
|
|
258
|
+
retry := retry.contents + 1
|
|
259
|
+
await Utils.delay(retryInterval)
|
|
192
260
|
}
|
|
193
|
-
} catch {
|
|
194
|
-
| exn =>
|
|
195
|
-
let retryInterval = sourceManager.getHeightRetryInterval(~retry=retry.contents)
|
|
196
|
-
logger->Logging.childTrace({
|
|
197
|
-
"msg": `Height retrieval from ${source.name} source failed. Retrying in ${retryInterval->Int.toString}ms.`,
|
|
198
|
-
"source": source.name,
|
|
199
|
-
"err": exn->Utils.prettifyExn,
|
|
200
|
-
})
|
|
201
|
-
retry := retry.contents + 1
|
|
202
|
-
await Utils.delay(retryInterval)
|
|
203
261
|
}
|
|
204
262
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
263
|
+
|
|
264
|
+
// Update Prometheus only if height increased
|
|
265
|
+
if newHeight.contents > initialHeight {
|
|
266
|
+
Prometheus.SourceHeight.set(
|
|
267
|
+
~sourceName=source.name,
|
|
268
|
+
~chainId=source.chain->ChainMap.Chain.toChainId,
|
|
269
|
+
~blockNumber=newHeight.contents,
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
|
|
210
273
|
newHeight.contents
|
|
211
274
|
}
|
|
212
275
|
|
|
213
276
|
// Polls for a block height greater than the given block number to ensure a new block is available for indexing.
|
|
214
277
|
let waitForNewBlock = async (sourceManager: t, ~knownHeight) => {
|
|
215
|
-
let {
|
|
278
|
+
let {sourcesState} = sourceManager
|
|
216
279
|
|
|
217
280
|
let logger = Logging.createChild(
|
|
218
281
|
~params={
|
|
@@ -230,8 +293,12 @@ let waitForNewBlock = async (sourceManager: t, ~knownHeight) => {
|
|
|
230
293
|
|
|
231
294
|
let syncSources = []
|
|
232
295
|
let fallbackSources = []
|
|
233
|
-
|
|
234
|
-
|
|
296
|
+
sourcesState->Array.forEach(sourceState => {
|
|
297
|
+
let source = sourceState.source
|
|
298
|
+
if sourceState.disabled {
|
|
299
|
+
// Skip disabled sources
|
|
300
|
+
()
|
|
301
|
+
} else if (
|
|
235
302
|
source.sourceFor === Sync ||
|
|
236
303
|
// Include Live sources only after initial sync has started
|
|
237
304
|
// Live sources are optimized for real-time indexing with lower latency
|
|
@@ -241,9 +308,9 @@ let waitForNewBlock = async (sourceManager: t, ~knownHeight) => {
|
|
|
241
308
|
// if all main sync sources are still not valid
|
|
242
309
|
source === sourceManager.activeSource
|
|
243
310
|
) {
|
|
244
|
-
syncSources->Array.push(
|
|
311
|
+
syncSources->Array.push(sourceState)
|
|
245
312
|
} else {
|
|
246
|
-
fallbackSources->Array.push(
|
|
313
|
+
fallbackSources->Array.push(sourceState)
|
|
247
314
|
}
|
|
248
315
|
})
|
|
249
316
|
|
|
@@ -251,8 +318,11 @@ let waitForNewBlock = async (sourceManager: t, ~knownHeight) => {
|
|
|
251
318
|
|
|
252
319
|
let (source, newBlockHeight) = await Promise.race(
|
|
253
320
|
syncSources
|
|
254
|
-
->Array.map(async
|
|
255
|
-
(
|
|
321
|
+
->Array.map(async sourceState => {
|
|
322
|
+
(
|
|
323
|
+
sourceState.source,
|
|
324
|
+
await sourceManager->getSourceNewHeight(~sourceState, ~knownHeight, ~status, ~logger),
|
|
325
|
+
)
|
|
256
326
|
})
|
|
257
327
|
->Array.concat([
|
|
258
328
|
Utils.delay(sourceManager.newBlockFallbackStallTimeout)->Promise.then(() => {
|
|
@@ -275,10 +345,10 @@ let waitForNewBlock = async (sourceManager: t, ~knownHeight) => {
|
|
|
275
345
|
// Promise.race will be forever pending if fallbackSources is empty
|
|
276
346
|
// which is good for this use case
|
|
277
347
|
Promise.race(
|
|
278
|
-
fallbackSources->Array.map(async
|
|
348
|
+
fallbackSources->Array.map(async sourceState => {
|
|
279
349
|
(
|
|
280
|
-
source,
|
|
281
|
-
await sourceManager->getSourceNewHeight(~
|
|
350
|
+
sourceState.source,
|
|
351
|
+
await sourceManager->getSourceNewHeight(~sourceState, ~knownHeight, ~status, ~logger),
|
|
282
352
|
)
|
|
283
353
|
}),
|
|
284
354
|
)
|
|
@@ -301,11 +371,11 @@ let waitForNewBlock = async (sourceManager: t, ~knownHeight) => {
|
|
|
301
371
|
newBlockHeight
|
|
302
372
|
}
|
|
303
373
|
|
|
304
|
-
let
|
|
374
|
+
let getNextSyncSourceState = (
|
|
305
375
|
sourceManager,
|
|
306
376
|
// This is needed to include the Fallback source to rotation
|
|
307
|
-
~
|
|
308
|
-
~
|
|
377
|
+
~initialSourceState: sourceState,
|
|
378
|
+
~currentSourceState: sourceState,
|
|
309
379
|
// After multiple failures start returning fallback sources as well
|
|
310
380
|
// But don't try it when main sync sources fail because of invalid configuration
|
|
311
381
|
// note: The logic might be changed in the future
|
|
@@ -316,18 +386,23 @@ let getNextSyncSource = (
|
|
|
316
386
|
|
|
317
387
|
let hasActive = ref(false)
|
|
318
388
|
|
|
319
|
-
sourceManager.
|
|
320
|
-
|
|
389
|
+
sourceManager.sourcesState->Array.forEach(sourceState => {
|
|
390
|
+
let source = sourceState.source
|
|
391
|
+
|
|
392
|
+
// Skip disabled sources
|
|
393
|
+
if sourceState.disabled {
|
|
394
|
+
()
|
|
395
|
+
} else if sourceState === currentSourceState {
|
|
321
396
|
hasActive := true
|
|
322
397
|
} else if (
|
|
323
398
|
switch source.sourceFor {
|
|
324
399
|
| Sync => true
|
|
325
400
|
// Live sources should NOT be used for historical sync rotation
|
|
326
401
|
// They are only meant for real-time indexing once synced
|
|
327
|
-
| Live | Fallback => attemptFallbacks ||
|
|
402
|
+
| Live | Fallback => attemptFallbacks || sourceState === initialSourceState
|
|
328
403
|
}
|
|
329
404
|
) {
|
|
330
|
-
(hasActive.contents ? after : before)->Array.push(
|
|
405
|
+
(hasActive.contents ? after : before)->Array.push(sourceState)
|
|
331
406
|
}
|
|
332
407
|
})
|
|
333
408
|
|
|
@@ -336,7 +411,7 @@ let getNextSyncSource = (
|
|
|
336
411
|
| None =>
|
|
337
412
|
switch before->Array.get(0) {
|
|
338
413
|
| Some(s) => s
|
|
339
|
-
| None =>
|
|
414
|
+
| None => currentSourceState
|
|
340
415
|
}
|
|
341
416
|
}
|
|
342
417
|
}
|
|
@@ -352,12 +427,16 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~knownHeig
|
|
|
352
427
|
)
|
|
353
428
|
let responseRef = ref(None)
|
|
354
429
|
let retryRef = ref(0)
|
|
355
|
-
let
|
|
356
|
-
|
|
430
|
+
let initialSourceState =
|
|
431
|
+
sourceManager.sourcesState
|
|
432
|
+
->Js.Array2.find(s => s.source === sourceManager.activeSource)
|
|
433
|
+
->Option.getUnsafe
|
|
434
|
+
let sourceStateRef = ref(initialSourceState)
|
|
357
435
|
let shouldUpdateActiveSource = ref(false)
|
|
358
436
|
|
|
359
437
|
while responseRef.contents->Option.isNone {
|
|
360
|
-
let
|
|
438
|
+
let sourceState = sourceStateRef.contents
|
|
439
|
+
let source = sourceState.source
|
|
361
440
|
let toBlock = toBlockRef.contents
|
|
362
441
|
let retry = retryRef.contents
|
|
363
442
|
|
|
@@ -398,15 +477,19 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~knownHeig
|
|
|
398
477
|
switch error {
|
|
399
478
|
| UnsupportedSelection(_)
|
|
400
479
|
| FailedGettingFieldSelection(_) => {
|
|
401
|
-
let
|
|
480
|
+
let nextSourceState =
|
|
481
|
+
sourceManager->getNextSyncSourceState(
|
|
482
|
+
~initialSourceState,
|
|
483
|
+
~currentSourceState=sourceState,
|
|
484
|
+
)
|
|
402
485
|
|
|
403
|
-
// These errors are impossible to recover, so we
|
|
404
|
-
//
|
|
405
|
-
let
|
|
486
|
+
// These errors are impossible to recover, so we disable the source
|
|
487
|
+
// so it's not attempted anymore
|
|
488
|
+
let notAlreadyDisabled = disableSource(sourceState)
|
|
406
489
|
|
|
407
490
|
// In case there are multiple partitions
|
|
408
491
|
// failing at the same time. Log only once
|
|
409
|
-
if
|
|
492
|
+
if notAlreadyDisabled {
|
|
410
493
|
switch error {
|
|
411
494
|
| UnsupportedSelection({message}) => logger->Logging.childError(message)
|
|
412
495
|
| FailedGettingFieldSelection({exn, message, blockNumber, logIndex}) =>
|
|
@@ -420,7 +503,7 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~knownHeig
|
|
|
420
503
|
}
|
|
421
504
|
}
|
|
422
505
|
|
|
423
|
-
if
|
|
506
|
+
if nextSourceState === sourceState {
|
|
424
507
|
%raw(`null`)->ErrorHandling.mkLogAndRaise(
|
|
425
508
|
~logger,
|
|
426
509
|
~msg="The indexer doesn't have data-sources which can continue fetching. Please, check the error logs or reach out to the Envio team.",
|
|
@@ -428,9 +511,9 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~knownHeig
|
|
|
428
511
|
} else {
|
|
429
512
|
logger->Logging.childInfo({
|
|
430
513
|
"msg": "Switching to another data-source",
|
|
431
|
-
"source":
|
|
514
|
+
"source": nextSourceState.source.name,
|
|
432
515
|
})
|
|
433
|
-
|
|
516
|
+
sourceStateRef := nextSourceState
|
|
434
517
|
shouldUpdateActiveSource := true
|
|
435
518
|
retryRef := 0
|
|
436
519
|
}
|
|
@@ -444,14 +527,14 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~knownHeig
|
|
|
444
527
|
toBlockRef := Some(toBlock)
|
|
445
528
|
retryRef := 0
|
|
446
529
|
| FailedGettingItems({exn, attemptedToBlock, retry: ImpossibleForTheQuery({message})}) =>
|
|
447
|
-
let
|
|
448
|
-
sourceManager->
|
|
449
|
-
~
|
|
450
|
-
~
|
|
530
|
+
let nextSourceState =
|
|
531
|
+
sourceManager->getNextSyncSourceState(
|
|
532
|
+
~initialSourceState,
|
|
533
|
+
~currentSourceState=sourceState,
|
|
451
534
|
~attemptFallbacks=true,
|
|
452
535
|
)
|
|
453
536
|
|
|
454
|
-
let hasAnotherSource =
|
|
537
|
+
let hasAnotherSource = nextSourceState !== initialSourceState
|
|
455
538
|
|
|
456
539
|
logger->Logging.childWarn({
|
|
457
540
|
"msg": message ++ (hasAnotherSource ? " - Attempting to another source" : ""),
|
|
@@ -465,7 +548,7 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~knownHeig
|
|
|
465
548
|
~msg="The indexer doesn't have data-sources which can continue fetching. Please, check the error logs or reach out to the Envio team.",
|
|
466
549
|
)
|
|
467
550
|
} else {
|
|
468
|
-
|
|
551
|
+
sourceStateRef := nextSourceState
|
|
469
552
|
shouldUpdateActiveSource := false
|
|
470
553
|
retryRef := 0
|
|
471
554
|
}
|
|
@@ -480,19 +563,19 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~knownHeig
|
|
|
480
563
|
// just keep the value high
|
|
481
564
|
let attemptFallbacks = retry >= 10
|
|
482
565
|
|
|
483
|
-
let
|
|
566
|
+
let nextSourceState = switch retry {
|
|
484
567
|
// Don't attempt a switch on first two failure
|
|
485
|
-
| 0 | 1 =>
|
|
568
|
+
| 0 | 1 => sourceState
|
|
486
569
|
| _ =>
|
|
487
570
|
// Then try to switch every second failure
|
|
488
571
|
if retry->mod(2) === 0 {
|
|
489
|
-
sourceManager->
|
|
490
|
-
~
|
|
572
|
+
sourceManager->getNextSyncSourceState(
|
|
573
|
+
~initialSourceState,
|
|
491
574
|
~attemptFallbacks,
|
|
492
|
-
~
|
|
575
|
+
~currentSourceState=sourceState,
|
|
493
576
|
)
|
|
494
577
|
} else {
|
|
495
|
-
|
|
578
|
+
sourceState
|
|
496
579
|
}
|
|
497
580
|
}
|
|
498
581
|
|
|
@@ -506,13 +589,13 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~knownHeig
|
|
|
506
589
|
"err": exn->Utils.prettifyExn,
|
|
507
590
|
})
|
|
508
591
|
|
|
509
|
-
let shouldSwitch =
|
|
592
|
+
let shouldSwitch = nextSourceState !== sourceState
|
|
510
593
|
if shouldSwitch {
|
|
511
594
|
logger->Logging.childInfo({
|
|
512
595
|
"msg": "Switching to another data-source",
|
|
513
|
-
"source":
|
|
596
|
+
"source": nextSourceState.source.name,
|
|
514
597
|
})
|
|
515
|
-
|
|
598
|
+
sourceStateRef := nextSourceState
|
|
516
599
|
shouldUpdateActiveSource := true
|
|
517
600
|
} else {
|
|
518
601
|
await Utils.delay(Pervasives.min(backoffMillis, 60_000))
|
|
@@ -526,7 +609,7 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~knownHeig
|
|
|
526
609
|
}
|
|
527
610
|
|
|
528
611
|
if shouldUpdateActiveSource.contents {
|
|
529
|
-
sourceManager.activeSource =
|
|
612
|
+
sourceManager.activeSource = sourceStateRef.contents.source
|
|
530
613
|
}
|
|
531
614
|
|
|
532
615
|
responseRef.contents->Option.getUnsafe
|