envio 2.21.0 → 2.21.2
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 +5 -5
- package/src/FetchState.res +1320 -0
- package/src/Prometheus.res +45 -1
- package/src/ReorgDetection.res +104 -56
- package/src/sources/Source.res +59 -0
- package/src/sources/SourceManager.res +493 -0
- package/src/sources/SourceManager.resi +32 -0
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
open Belt
|
|
2
|
+
|
|
3
|
+
type sourceManagerStatus = Idle | WaitingForNewBlock | Querieng
|
|
4
|
+
|
|
5
|
+
// Ideally the ChainFetcher name suits this better
|
|
6
|
+
// But currently the ChainFetcher module is immutable
|
|
7
|
+
// and handles both processing and fetching.
|
|
8
|
+
// So this module is to encapsulate the fetching logic only
|
|
9
|
+
// with a mutable state for easier reasoning and testing.
|
|
10
|
+
type t = {
|
|
11
|
+
sources: Utils.Set.t<Source.t>,
|
|
12
|
+
mutable statusStart: Hrtime.timeRef,
|
|
13
|
+
mutable status: sourceManagerStatus,
|
|
14
|
+
maxPartitionConcurrency: int,
|
|
15
|
+
newBlockFallbackStallTimeout: int,
|
|
16
|
+
stalledPollingInterval: int,
|
|
17
|
+
getHeightRetryInterval: (~retry: int) => int,
|
|
18
|
+
mutable activeSource: Source.t,
|
|
19
|
+
mutable waitingForNewBlockStateId: option<int>,
|
|
20
|
+
// Should take into consideration partitions fetching for previous states (before rollback)
|
|
21
|
+
mutable fetchingPartitionsCount: int,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let getActiveSource = sourceManager => sourceManager.activeSource
|
|
25
|
+
|
|
26
|
+
let makeGetHeightRetryInterval = (
|
|
27
|
+
~initialRetryInterval,
|
|
28
|
+
~backoffMultiplicative,
|
|
29
|
+
~maxRetryInterval,
|
|
30
|
+
) => {
|
|
31
|
+
(~retry: int) => {
|
|
32
|
+
let backoff = if retry === 0 {
|
|
33
|
+
1
|
|
34
|
+
} else {
|
|
35
|
+
retry * backoffMultiplicative
|
|
36
|
+
}
|
|
37
|
+
Pervasives.min(initialRetryInterval * backoff, maxRetryInterval)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let make = (
|
|
42
|
+
~sources: array<Source.t>,
|
|
43
|
+
~maxPartitionConcurrency,
|
|
44
|
+
~newBlockFallbackStallTimeout=20_000,
|
|
45
|
+
~stalledPollingInterval=5_000,
|
|
46
|
+
~getHeightRetryInterval=makeGetHeightRetryInterval(
|
|
47
|
+
~initialRetryInterval=1000,
|
|
48
|
+
~backoffMultiplicative=2,
|
|
49
|
+
~maxRetryInterval=60_000,
|
|
50
|
+
),
|
|
51
|
+
) => {
|
|
52
|
+
let initialActiveSource = switch sources->Js.Array2.find(source => source.sourceFor === Sync) {
|
|
53
|
+
| None => Js.Exn.raiseError("Invalid configuration, no data-source for historical sync provided")
|
|
54
|
+
| Some(source) => source
|
|
55
|
+
}
|
|
56
|
+
Prometheus.IndexingMaxConcurrency.set(
|
|
57
|
+
~maxConcurrency=maxPartitionConcurrency,
|
|
58
|
+
~chainId=initialActiveSource.chain->ChainMap.Chain.toChainId,
|
|
59
|
+
)
|
|
60
|
+
Prometheus.IndexingConcurrency.set(
|
|
61
|
+
~concurrency=0,
|
|
62
|
+
~chainId=initialActiveSource.chain->ChainMap.Chain.toChainId,
|
|
63
|
+
)
|
|
64
|
+
{
|
|
65
|
+
maxPartitionConcurrency,
|
|
66
|
+
sources: Utils.Set.fromArray(sources),
|
|
67
|
+
activeSource: initialActiveSource,
|
|
68
|
+
waitingForNewBlockStateId: None,
|
|
69
|
+
fetchingPartitionsCount: 0,
|
|
70
|
+
newBlockFallbackStallTimeout,
|
|
71
|
+
stalledPollingInterval,
|
|
72
|
+
getHeightRetryInterval,
|
|
73
|
+
statusStart: Hrtime.makeTimer(),
|
|
74
|
+
status: Idle,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let trackNewStatus = (sourceManager: t, ~newStatus) => {
|
|
79
|
+
let promCounter = switch newStatus {
|
|
80
|
+
| Idle => Prometheus.IndexingIdleTime.counter
|
|
81
|
+
| WaitingForNewBlock => Prometheus.IndexingSourceWaitingTime.counter
|
|
82
|
+
| Querieng => Prometheus.IndexingQueryTime.counter
|
|
83
|
+
}
|
|
84
|
+
promCounter->Prometheus.SafeCounter.incrementMany(
|
|
85
|
+
~labels=sourceManager.activeSource.chain->ChainMap.Chain.toChainId,
|
|
86
|
+
~value=sourceManager.statusStart->Hrtime.timeSince->Hrtime.toMillis->Hrtime.intFromMillis,
|
|
87
|
+
)
|
|
88
|
+
sourceManager.statusStart = Hrtime.makeTimer()
|
|
89
|
+
sourceManager.status = newStatus
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let fetchNext = async (
|
|
93
|
+
sourceManager: t,
|
|
94
|
+
~fetchState: FetchState.t,
|
|
95
|
+
~currentBlockHeight,
|
|
96
|
+
~executeQuery,
|
|
97
|
+
~waitForNewBlock,
|
|
98
|
+
~onNewBlock,
|
|
99
|
+
~maxPerChainQueueSize,
|
|
100
|
+
~stateId,
|
|
101
|
+
) => {
|
|
102
|
+
let {maxPartitionConcurrency} = sourceManager
|
|
103
|
+
|
|
104
|
+
switch fetchState->FetchState.getNextQuery(
|
|
105
|
+
~concurrencyLimit={
|
|
106
|
+
maxPartitionConcurrency - sourceManager.fetchingPartitionsCount
|
|
107
|
+
},
|
|
108
|
+
~maxQueueSize=maxPerChainQueueSize,
|
|
109
|
+
~currentBlockHeight,
|
|
110
|
+
~stateId,
|
|
111
|
+
) {
|
|
112
|
+
| ReachedMaxConcurrency
|
|
113
|
+
| NothingToQuery => ()
|
|
114
|
+
| WaitingForNewBlock =>
|
|
115
|
+
switch sourceManager.waitingForNewBlockStateId {
|
|
116
|
+
| Some(waitingStateId) if waitingStateId >= stateId => ()
|
|
117
|
+
| Some(_) // Case for the prev state before a rollback
|
|
118
|
+
| None =>
|
|
119
|
+
sourceManager->trackNewStatus(~newStatus=WaitingForNewBlock)
|
|
120
|
+
sourceManager.waitingForNewBlockStateId = Some(stateId)
|
|
121
|
+
let currentBlockHeight = await waitForNewBlock(~currentBlockHeight)
|
|
122
|
+
switch sourceManager.waitingForNewBlockStateId {
|
|
123
|
+
| Some(waitingStateId) if waitingStateId === stateId => {
|
|
124
|
+
sourceManager->trackNewStatus(~newStatus=Idle)
|
|
125
|
+
sourceManager.waitingForNewBlockStateId = None
|
|
126
|
+
onNewBlock(~currentBlockHeight)
|
|
127
|
+
}
|
|
128
|
+
| Some(_) // Don't reset it if we are waiting for another state
|
|
129
|
+
| None => ()
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
| Ready(queries) => {
|
|
133
|
+
fetchState->FetchState.startFetchingQueries(~queries, ~stateId)
|
|
134
|
+
sourceManager.fetchingPartitionsCount =
|
|
135
|
+
sourceManager.fetchingPartitionsCount + queries->Array.length
|
|
136
|
+
Prometheus.IndexingConcurrency.set(
|
|
137
|
+
~concurrency=sourceManager.fetchingPartitionsCount,
|
|
138
|
+
~chainId=sourceManager.activeSource.chain->ChainMap.Chain.toChainId,
|
|
139
|
+
)
|
|
140
|
+
sourceManager->trackNewStatus(~newStatus=Querieng)
|
|
141
|
+
let _ =
|
|
142
|
+
await queries
|
|
143
|
+
->Array.map(q => {
|
|
144
|
+
let promise = q->executeQuery
|
|
145
|
+
let _ = promise->Promise.thenResolve(_ => {
|
|
146
|
+
sourceManager.fetchingPartitionsCount = sourceManager.fetchingPartitionsCount - 1
|
|
147
|
+
Prometheus.IndexingConcurrency.set(
|
|
148
|
+
~concurrency=sourceManager.fetchingPartitionsCount,
|
|
149
|
+
~chainId=sourceManager.activeSource.chain->ChainMap.Chain.toChainId,
|
|
150
|
+
)
|
|
151
|
+
if sourceManager.fetchingPartitionsCount === 0 {
|
|
152
|
+
sourceManager->trackNewStatus(~newStatus=Idle)
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
promise
|
|
156
|
+
})
|
|
157
|
+
->Promise.all
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
type status = Active | Stalled | Done
|
|
163
|
+
|
|
164
|
+
let getSourceNewHeight = async (
|
|
165
|
+
sourceManager,
|
|
166
|
+
~source: Source.t,
|
|
167
|
+
~currentBlockHeight,
|
|
168
|
+
~status: ref<status>,
|
|
169
|
+
~logger,
|
|
170
|
+
) => {
|
|
171
|
+
let newHeight = ref(0)
|
|
172
|
+
let retry = ref(0)
|
|
173
|
+
|
|
174
|
+
while newHeight.contents <= currentBlockHeight && status.contents !== Done {
|
|
175
|
+
try {
|
|
176
|
+
// Use to detect if the source is taking too long to respond
|
|
177
|
+
let endTimer = Prometheus.SourceGetHeightDuration.startTimer({
|
|
178
|
+
"source": source.name,
|
|
179
|
+
"chainId": source.chain->ChainMap.Chain.toChainId,
|
|
180
|
+
})
|
|
181
|
+
let height = await source.getHeightOrThrow()
|
|
182
|
+
endTimer()
|
|
183
|
+
|
|
184
|
+
newHeight := height
|
|
185
|
+
if height <= currentBlockHeight {
|
|
186
|
+
retry := 0
|
|
187
|
+
// Slowdown polling when the chain isn't progressing
|
|
188
|
+
let pollingInterval = if status.contents === Stalled {
|
|
189
|
+
sourceManager.stalledPollingInterval
|
|
190
|
+
} else {
|
|
191
|
+
source.pollingInterval
|
|
192
|
+
}
|
|
193
|
+
await Utils.delay(pollingInterval)
|
|
194
|
+
}
|
|
195
|
+
} catch {
|
|
196
|
+
| exn =>
|
|
197
|
+
let retryInterval = sourceManager.getHeightRetryInterval(~retry=retry.contents)
|
|
198
|
+
logger->Logging.childTrace({
|
|
199
|
+
"msg": `Height retrieval from ${source.name} source failed. Retrying in ${retryInterval->Int.toString}ms.`,
|
|
200
|
+
"source": source.name,
|
|
201
|
+
"err": exn->Internal.prettifyExn,
|
|
202
|
+
})
|
|
203
|
+
retry := retry.contents + 1
|
|
204
|
+
await Utils.delay(retryInterval)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
Prometheus.SourceHeight.set(
|
|
208
|
+
~sourceName=source.name,
|
|
209
|
+
~chainId=source.chain->ChainMap.Chain.toChainId,
|
|
210
|
+
~blockNumber=newHeight.contents,
|
|
211
|
+
)
|
|
212
|
+
newHeight.contents
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Polls for a block height greater than the given block number to ensure a new block is available for indexing.
|
|
216
|
+
let waitForNewBlock = async (sourceManager: t, ~currentBlockHeight) => {
|
|
217
|
+
let {sources} = sourceManager
|
|
218
|
+
|
|
219
|
+
let logger = Logging.createChild(
|
|
220
|
+
~params={
|
|
221
|
+
"chainId": sourceManager.activeSource.chain->ChainMap.Chain.toChainId,
|
|
222
|
+
"currentBlockHeight": currentBlockHeight,
|
|
223
|
+
},
|
|
224
|
+
)
|
|
225
|
+
logger->Logging.childTrace("Initiating check for new blocks.")
|
|
226
|
+
|
|
227
|
+
let syncSources = []
|
|
228
|
+
let fallbackSources = []
|
|
229
|
+
sources->Utils.Set.forEach(source => {
|
|
230
|
+
if (
|
|
231
|
+
source.sourceFor === Sync ||
|
|
232
|
+
// Even if the active source is a fallback, still include
|
|
233
|
+
// it to the list. So we don't wait for a timeout again
|
|
234
|
+
// if all main sync sources are still not valid
|
|
235
|
+
source === sourceManager.activeSource
|
|
236
|
+
) {
|
|
237
|
+
syncSources->Array.push(source)
|
|
238
|
+
} else {
|
|
239
|
+
fallbackSources->Array.push(source)
|
|
240
|
+
}
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
let status = ref(Active)
|
|
244
|
+
|
|
245
|
+
let (source, newBlockHeight) = await Promise.race(
|
|
246
|
+
syncSources
|
|
247
|
+
->Array.map(async source => {
|
|
248
|
+
(
|
|
249
|
+
source,
|
|
250
|
+
await sourceManager->getSourceNewHeight(~source, ~currentBlockHeight, ~status, ~logger),
|
|
251
|
+
)
|
|
252
|
+
})
|
|
253
|
+
->Array.concat([
|
|
254
|
+
Utils.delay(sourceManager.newBlockFallbackStallTimeout)->Promise.then(() => {
|
|
255
|
+
if status.contents !== Done {
|
|
256
|
+
status := Stalled
|
|
257
|
+
|
|
258
|
+
switch fallbackSources {
|
|
259
|
+
| [] =>
|
|
260
|
+
logger->Logging.childWarn(
|
|
261
|
+
`No new blocks detected within ${(sourceManager.newBlockFallbackStallTimeout / 1000)
|
|
262
|
+
->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`,
|
|
263
|
+
)
|
|
264
|
+
| _ =>
|
|
265
|
+
logger->Logging.childWarn(
|
|
266
|
+
`No new blocks detected within ${(sourceManager.newBlockFallbackStallTimeout / 1000)
|
|
267
|
+
->Int.toString}s. Continuing polling with fallback RPC sources from the configuration.`,
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// Promise.race will be forever pending if fallbackSources is empty
|
|
272
|
+
// which is good for this use case
|
|
273
|
+
Promise.race(
|
|
274
|
+
fallbackSources->Array.map(async source => {
|
|
275
|
+
(
|
|
276
|
+
source,
|
|
277
|
+
await sourceManager->getSourceNewHeight(
|
|
278
|
+
~source,
|
|
279
|
+
~currentBlockHeight,
|
|
280
|
+
~status,
|
|
281
|
+
~logger,
|
|
282
|
+
),
|
|
283
|
+
)
|
|
284
|
+
}),
|
|
285
|
+
)
|
|
286
|
+
}),
|
|
287
|
+
]),
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
sourceManager.activeSource = source
|
|
291
|
+
|
|
292
|
+
// Show a higher level log if we displayed a warning/error after newBlockFallbackStallTimeout
|
|
293
|
+
let log = status.contents === Stalled ? Logging.childInfo : Logging.childTrace
|
|
294
|
+
logger->log({
|
|
295
|
+
"msg": `New blocks successfully found.`,
|
|
296
|
+
"source": source.name,
|
|
297
|
+
"newBlockHeight": newBlockHeight,
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
status := Done
|
|
301
|
+
|
|
302
|
+
newBlockHeight
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
let getNextSyncSource = (
|
|
306
|
+
sourceManager,
|
|
307
|
+
// This is needed to include the Fallback source to rotation
|
|
308
|
+
~initialSource,
|
|
309
|
+
// After multiple failures start returning fallback sources as well
|
|
310
|
+
// But don't try it when main sync sources fail because of invalid configuration
|
|
311
|
+
// note: The logic might be changed in the future
|
|
312
|
+
~attemptFallbacks=false,
|
|
313
|
+
) => {
|
|
314
|
+
let before = []
|
|
315
|
+
let after = []
|
|
316
|
+
|
|
317
|
+
let hasActive = ref(false)
|
|
318
|
+
|
|
319
|
+
sourceManager.sources->Utils.Set.forEach(source => {
|
|
320
|
+
if source === sourceManager.activeSource {
|
|
321
|
+
hasActive := true
|
|
322
|
+
} else if (
|
|
323
|
+
switch source.sourceFor {
|
|
324
|
+
| Sync => true
|
|
325
|
+
| Fallback => attemptFallbacks || source === initialSource
|
|
326
|
+
}
|
|
327
|
+
) {
|
|
328
|
+
(hasActive.contents ? after : before)->Array.push(source)
|
|
329
|
+
}
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
switch after->Array.get(0) {
|
|
333
|
+
| Some(s) => s
|
|
334
|
+
| None =>
|
|
335
|
+
switch before->Array.get(0) {
|
|
336
|
+
| Some(s) => s
|
|
337
|
+
| None => sourceManager.activeSource
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~currentBlockHeight) => {
|
|
343
|
+
let toBlockRef = ref(
|
|
344
|
+
switch query.target {
|
|
345
|
+
| Head => None
|
|
346
|
+
| EndBlock({toBlock})
|
|
347
|
+
| Merge({toBlock}) =>
|
|
348
|
+
Some(toBlock)
|
|
349
|
+
},
|
|
350
|
+
)
|
|
351
|
+
let responseRef = ref(None)
|
|
352
|
+
let retryRef = ref(0)
|
|
353
|
+
let initialSource = sourceManager.activeSource
|
|
354
|
+
|
|
355
|
+
while responseRef.contents->Option.isNone {
|
|
356
|
+
let source = sourceManager.activeSource
|
|
357
|
+
let toBlock = toBlockRef.contents
|
|
358
|
+
let retry = retryRef.contents
|
|
359
|
+
|
|
360
|
+
let logger = Logging.createChild(
|
|
361
|
+
~params={
|
|
362
|
+
"chainId": source.chain->ChainMap.Chain.toChainId,
|
|
363
|
+
"logType": "Block Range Query",
|
|
364
|
+
"partitionId": query.partitionId,
|
|
365
|
+
"source": source.name,
|
|
366
|
+
"fromBlock": query.fromBlock,
|
|
367
|
+
"toBlock": toBlock,
|
|
368
|
+
"addresses": query.addressesByContractName->FetchState.addressesByContractNameCount,
|
|
369
|
+
"retry": retry,
|
|
370
|
+
},
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
let response = await source.getItemsOrThrow(
|
|
375
|
+
~fromBlock=query.fromBlock,
|
|
376
|
+
~toBlock,
|
|
377
|
+
~addressesByContractName=query.addressesByContractName,
|
|
378
|
+
~indexingContracts=query.indexingContracts,
|
|
379
|
+
~partitionId=query.partitionId,
|
|
380
|
+
~currentBlockHeight,
|
|
381
|
+
~selection=query.selection,
|
|
382
|
+
~retry,
|
|
383
|
+
~logger,
|
|
384
|
+
)
|
|
385
|
+
logger->Logging.childTrace({
|
|
386
|
+
"msg": "Fetched block range from server",
|
|
387
|
+
"toBlock": response.latestFetchedBlockNumber,
|
|
388
|
+
"numEvents": response.parsedQueueItems->Array.length,
|
|
389
|
+
"stats": response.stats,
|
|
390
|
+
})
|
|
391
|
+
responseRef := Some(response)
|
|
392
|
+
} catch {
|
|
393
|
+
| Source.GetItemsError(error) =>
|
|
394
|
+
switch error {
|
|
395
|
+
| UnsupportedSelection(_)
|
|
396
|
+
| FailedGettingFieldSelection(_)
|
|
397
|
+
| FailedParsingItems(_) => {
|
|
398
|
+
let nextSource = sourceManager->getNextSyncSource(~initialSource)
|
|
399
|
+
|
|
400
|
+
// These errors are impossible to recover, so we delete the source
|
|
401
|
+
// from sourceManager so it's not attempted anymore
|
|
402
|
+
let notAlreadyDeleted = sourceManager.sources->Utils.Set.delete(source)
|
|
403
|
+
|
|
404
|
+
// In case there are multiple partitions
|
|
405
|
+
// failing at the same time. Log only once
|
|
406
|
+
if notAlreadyDeleted {
|
|
407
|
+
switch error {
|
|
408
|
+
| UnsupportedSelection({message}) => logger->Logging.childError(message)
|
|
409
|
+
| FailedGettingFieldSelection({exn, message, blockNumber, logIndex})
|
|
410
|
+
| FailedParsingItems({exn, message, blockNumber, logIndex}) =>
|
|
411
|
+
logger->Logging.childError({
|
|
412
|
+
"msg": message,
|
|
413
|
+
"err": exn->Internal.prettifyExn,
|
|
414
|
+
"blockNumber": blockNumber,
|
|
415
|
+
"logIndex": logIndex,
|
|
416
|
+
})
|
|
417
|
+
| _ => ()
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if nextSource === source {
|
|
422
|
+
%raw(`null`)->ErrorHandling.mkLogAndRaise(
|
|
423
|
+
~logger,
|
|
424
|
+
~msg="The indexer doesn't have data-sources which can continue fetching. Please, check the error logs or reach out to the Envio team.",
|
|
425
|
+
)
|
|
426
|
+
} else {
|
|
427
|
+
logger->Logging.childInfo({
|
|
428
|
+
"msg": "Switching to another data-source",
|
|
429
|
+
"source": nextSource.name,
|
|
430
|
+
})
|
|
431
|
+
sourceManager.activeSource = nextSource
|
|
432
|
+
retryRef := 0
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
| FailedGettingItems({attemptedToBlock, retry: WithSuggestedToBlock({toBlock})}) =>
|
|
436
|
+
logger->Logging.childTrace({
|
|
437
|
+
"msg": "Failed getting data for the block range. Immediately retrying with the suggested block range from response.",
|
|
438
|
+
"toBlock": attemptedToBlock,
|
|
439
|
+
"suggestedToBlock": toBlock,
|
|
440
|
+
})
|
|
441
|
+
toBlockRef := Some(toBlock)
|
|
442
|
+
retryRef := 0
|
|
443
|
+
| FailedGettingItems({exn, attemptedToBlock, retry: WithBackoff({message, backoffMillis})}) =>
|
|
444
|
+
// Starting from the 11th failure (retry=10)
|
|
445
|
+
// include fallback sources for switch
|
|
446
|
+
// (previously it would consider only sync sources or the initial one)
|
|
447
|
+
// This is a little bit tricky to find the right number,
|
|
448
|
+
// because meaning between RPC and HyperSync is different for the error
|
|
449
|
+
// but since Fallback was initially designed to be used only for height check
|
|
450
|
+
// just keep the value high
|
|
451
|
+
let attemptFallbacks = retry >= 10
|
|
452
|
+
|
|
453
|
+
let nextSource = switch retry {
|
|
454
|
+
// Don't attempt a switch on first two failure
|
|
455
|
+
| 0 | 1 => source
|
|
456
|
+
| _ =>
|
|
457
|
+
// Then try to switch every second failure
|
|
458
|
+
if retry->mod(2) === 0 {
|
|
459
|
+
sourceManager->getNextSyncSource(~initialSource, ~attemptFallbacks)
|
|
460
|
+
} else {
|
|
461
|
+
source
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Start displaying warnings after 4 failures
|
|
466
|
+
let log = retry >= 4 ? Logging.childWarn : Logging.childTrace
|
|
467
|
+
logger->log({
|
|
468
|
+
"msg": message,
|
|
469
|
+
"toBlock": attemptedToBlock,
|
|
470
|
+
"backOffMilliseconds": backoffMillis,
|
|
471
|
+
"retry": retry,
|
|
472
|
+
"err": exn->Internal.prettifyExn,
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
let shouldSwitch = nextSource !== source
|
|
476
|
+
if shouldSwitch {
|
|
477
|
+
logger->Logging.childInfo({
|
|
478
|
+
"msg": "Switching to another data-source",
|
|
479
|
+
"source": nextSource.name,
|
|
480
|
+
})
|
|
481
|
+
sourceManager.activeSource = nextSource
|
|
482
|
+
} else {
|
|
483
|
+
await Utils.delay(Pervasives.min(backoffMillis, 60_000))
|
|
484
|
+
}
|
|
485
|
+
retryRef := retryRef.contents + 1
|
|
486
|
+
}
|
|
487
|
+
// TODO: Handle more error cases and hang/retry instead of throwing
|
|
488
|
+
| exn => exn->ErrorHandling.mkLogAndRaise(~logger, ~msg="Failed to fetch block Range")
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
responseRef.contents->Option.getUnsafe
|
|
493
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
type t
|
|
2
|
+
|
|
3
|
+
let make: (
|
|
4
|
+
~sources: array<Source.t>,
|
|
5
|
+
~maxPartitionConcurrency: int,
|
|
6
|
+
~newBlockFallbackStallTimeout: int=?,
|
|
7
|
+
~stalledPollingInterval: int=?,
|
|
8
|
+
~getHeightRetryInterval: (~retry: int) => int=?,
|
|
9
|
+
) => t
|
|
10
|
+
|
|
11
|
+
let getActiveSource: t => Source.t
|
|
12
|
+
|
|
13
|
+
let fetchNext: (
|
|
14
|
+
t,
|
|
15
|
+
~fetchState: FetchState.t,
|
|
16
|
+
~currentBlockHeight: int,
|
|
17
|
+
~executeQuery: FetchState.query => promise<unit>,
|
|
18
|
+
~waitForNewBlock: (~currentBlockHeight: int) => promise<int>,
|
|
19
|
+
~onNewBlock: (~currentBlockHeight: int) => unit,
|
|
20
|
+
~maxPerChainQueueSize: int,
|
|
21
|
+
~stateId: int,
|
|
22
|
+
) => promise<unit>
|
|
23
|
+
|
|
24
|
+
let waitForNewBlock: (t, ~currentBlockHeight: int) => promise<int>
|
|
25
|
+
|
|
26
|
+
let executeQuery: (
|
|
27
|
+
t,
|
|
28
|
+
~query: FetchState.query,
|
|
29
|
+
~currentBlockHeight: int,
|
|
30
|
+
) => promise<Source.blockRangeFetchResponse>
|
|
31
|
+
|
|
32
|
+
let makeGetHeightRetryInterval: (~initialRetryInterval: int, ~backoffMultiplicative: int, ~maxRetryInterval: int) => (~retry: int) => int
|