envio 3.0.0-alpha.2 → 3.0.0-alpha.20
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 +164 -30
- package/bin.mjs +49 -0
- package/evm.schema.json +79 -169
- package/fuel.schema.json +50 -21
- package/index.d.ts +497 -1
- package/index.js +4 -0
- package/package.json +42 -31
- package/rescript.json +4 -1
- package/src/Batch.res +11 -8
- package/src/Batch.res.mjs +11 -9
- package/src/ChainFetcher.res +531 -0
- package/src/ChainFetcher.res.mjs +339 -0
- package/src/ChainManager.res +190 -0
- package/src/ChainManager.res.mjs +166 -0
- package/src/Change.res +3 -3
- package/src/Config.gen.ts +19 -0
- package/src/Config.res +737 -22
- package/src/Config.res.mjs +703 -26
- package/src/{Indexer.res → Ctx.res} +1 -1
- package/src/Ecosystem.res +9 -124
- package/src/Ecosystem.res.mjs +19 -160
- package/src/Env.res +30 -74
- package/src/Env.res.mjs +25 -87
- package/src/Envio.gen.ts +3 -1
- package/src/Envio.res +20 -9
- package/src/EventProcessing.res +469 -0
- package/src/EventProcessing.res.mjs +337 -0
- package/src/EvmTypes.gen.ts +6 -0
- package/src/EvmTypes.res +1 -0
- package/src/FetchState.res +1256 -639
- package/src/FetchState.res.mjs +1135 -612
- package/src/GlobalState.res +1190 -0
- package/src/GlobalState.res.mjs +1183 -0
- package/src/GlobalStateManager.res +68 -0
- package/src/GlobalStateManager.res.mjs +75 -0
- package/src/GlobalStateManager.resi +7 -0
- package/src/HandlerLoader.res +89 -0
- package/src/HandlerLoader.res.mjs +79 -0
- package/src/HandlerRegister.res +357 -0
- package/src/HandlerRegister.res.mjs +299 -0
- package/src/{EventRegister.resi → HandlerRegister.resi} +13 -13
- package/src/Hasura.res +111 -175
- package/src/Hasura.res.mjs +88 -150
- package/src/InMemoryStore.res +1 -1
- package/src/InMemoryStore.res.mjs +3 -3
- package/src/InMemoryTable.res +1 -1
- package/src/InMemoryTable.res.mjs +1 -1
- package/src/Internal.gen.ts +4 -0
- package/src/Internal.res +230 -12
- package/src/Internal.res.mjs +115 -1
- package/src/LoadLayer.res +444 -0
- package/src/LoadLayer.res.mjs +296 -0
- package/src/LoadLayer.resi +32 -0
- package/src/LogSelection.res +33 -27
- package/src/LogSelection.res.mjs +6 -0
- package/src/Logging.res +21 -7
- package/src/Logging.res.mjs +16 -8
- package/src/Main.res +377 -0
- package/src/Main.res.mjs +339 -0
- package/src/Persistence.res +7 -21
- package/src/Persistence.res.mjs +3 -3
- package/src/PgStorage.gen.ts +10 -0
- package/src/PgStorage.res +116 -69
- package/src/PgStorage.res.d.mts +5 -0
- package/src/PgStorage.res.mjs +93 -50
- package/src/Prometheus.res +294 -224
- package/src/Prometheus.res.mjs +353 -340
- package/src/ReorgDetection.res +6 -10
- package/src/ReorgDetection.res.mjs +6 -6
- package/src/SafeCheckpointTracking.res +4 -4
- package/src/SafeCheckpointTracking.res.mjs +2 -2
- package/src/Sink.res +4 -2
- package/src/Sink.res.mjs +2 -1
- package/src/TableIndices.res +0 -1
- package/src/TestIndexer.res +692 -0
- package/src/TestIndexer.res.mjs +527 -0
- package/src/TestIndexerProxyStorage.res +205 -0
- package/src/TestIndexerProxyStorage.res.mjs +151 -0
- package/src/TopicFilter.res +1 -1
- package/src/Types.ts +1 -1
- package/src/UserContext.res +424 -0
- package/src/UserContext.res.mjs +279 -0
- package/src/Utils.res +97 -26
- package/src/Utils.res.mjs +91 -44
- package/src/bindings/BigInt.res +10 -0
- package/src/bindings/BigInt.res.mjs +15 -0
- package/src/bindings/ClickHouse.res +120 -23
- package/src/bindings/ClickHouse.res.mjs +118 -28
- package/src/bindings/DateFns.res +74 -0
- package/src/bindings/DateFns.res.mjs +22 -0
- package/src/bindings/EventSource.res +8 -1
- package/src/bindings/EventSource.res.mjs +8 -1
- package/src/bindings/Express.res +1 -0
- package/src/bindings/Hrtime.res +14 -1
- package/src/bindings/Hrtime.res.mjs +22 -2
- package/src/bindings/Hrtime.resi +4 -0
- package/src/bindings/Lodash.res +0 -1
- package/src/bindings/NodeJs.res +49 -3
- package/src/bindings/NodeJs.res.mjs +11 -3
- package/src/bindings/Pino.res +24 -10
- package/src/bindings/Pino.res.mjs +14 -8
- package/src/bindings/Postgres.gen.ts +8 -0
- package/src/bindings/Postgres.res +5 -1
- package/src/bindings/Postgres.res.d.mts +5 -0
- package/src/bindings/PromClient.res +0 -10
- package/src/bindings/PromClient.res.mjs +0 -3
- package/src/bindings/Vitest.res +142 -0
- package/src/bindings/Vitest.res.mjs +9 -0
- package/src/bindings/WebSocket.res +27 -0
- package/src/bindings/WebSocket.res.mjs +2 -0
- package/src/bindings/Yargs.res +8 -0
- package/src/bindings/Yargs.res.mjs +2 -0
- package/src/db/EntityHistory.res +7 -7
- package/src/db/EntityHistory.res.mjs +9 -9
- package/src/db/InternalTable.res +59 -111
- package/src/db/InternalTable.res.mjs +73 -104
- package/src/db/Table.res +27 -8
- package/src/db/Table.res.mjs +25 -14
- package/src/sources/Evm.res +84 -0
- package/src/sources/Evm.res.mjs +105 -0
- package/src/sources/EvmChain.res +94 -0
- package/src/sources/EvmChain.res.mjs +60 -0
- package/src/sources/Fuel.res +19 -34
- package/src/sources/Fuel.res.mjs +34 -16
- package/src/sources/FuelSDK.res +38 -0
- package/src/sources/FuelSDK.res.mjs +29 -0
- package/src/sources/HyperFuel.res +2 -2
- package/src/sources/HyperFuel.resi +1 -1
- package/src/sources/HyperFuelClient.res +2 -2
- package/src/sources/HyperFuelSource.res +33 -13
- package/src/sources/HyperFuelSource.res.mjs +24 -16
- package/src/sources/HyperSync.res +36 -6
- package/src/sources/HyperSync.res.mjs +9 -7
- package/src/sources/HyperSync.resi +4 -0
- package/src/sources/HyperSyncClient.res +1 -1
- package/src/sources/HyperSyncHeightStream.res +47 -116
- package/src/sources/HyperSyncHeightStream.res.mjs +46 -73
- package/src/sources/HyperSyncSource.res +118 -139
- package/src/sources/HyperSyncSource.res.mjs +104 -121
- package/src/sources/Rpc.res +86 -14
- package/src/sources/Rpc.res.mjs +101 -9
- package/src/sources/RpcSource.res +621 -364
- package/src/sources/RpcSource.res.mjs +843 -410
- package/src/sources/RpcWebSocketHeightStream.res +181 -0
- package/src/sources/RpcWebSocketHeightStream.res.mjs +196 -0
- package/src/sources/Source.res +7 -5
- package/src/sources/SourceManager.res +325 -225
- package/src/sources/SourceManager.res.mjs +314 -171
- package/src/sources/SourceManager.resi +17 -6
- package/src/sources/Svm.res +81 -0
- package/src/sources/Svm.res.mjs +90 -0
- package/src/tui/Tui.res +247 -0
- package/src/tui/Tui.res.mjs +337 -0
- package/src/tui/bindings/Ink.res +371 -0
- package/src/tui/bindings/Ink.res.mjs +72 -0
- package/src/tui/bindings/Style.res +123 -0
- package/src/tui/bindings/Style.res.mjs +2 -0
- package/src/tui/components/BufferedProgressBar.res +40 -0
- package/src/tui/components/BufferedProgressBar.res.mjs +57 -0
- package/src/tui/components/CustomHooks.res +122 -0
- package/src/tui/components/CustomHooks.res.mjs +179 -0
- package/src/tui/components/Messages.res +41 -0
- package/src/tui/components/Messages.res.mjs +75 -0
- package/src/tui/components/SyncETA.res +174 -0
- package/src/tui/components/SyncETA.res.mjs +263 -0
- package/src/tui/components/TuiData.res +47 -0
- package/src/tui/components/TuiData.res.mjs +34 -0
- package/svm.schema.json +112 -0
- package/bin.js +0 -48
- package/src/EventRegister.res +0 -241
- package/src/EventRegister.res.mjs +0 -240
- package/src/bindings/Ethers.gen.ts +0 -14
- package/src/bindings/Ethers.res +0 -204
- package/src/bindings/Ethers.res.mjs +0 -130
- /package/src/{Indexer.res.mjs → Ctx.res.mjs} +0 -0
|
@@ -2,27 +2,57 @@ 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
|
+
// Timestamp (ms) when this source last failed during executeQuery.
|
|
12
|
+
// Used to decide when to attempt recovery to this source.
|
|
13
|
+
mutable lastFailedAt: option<float>,
|
|
14
|
+
}
|
|
15
|
+
|
|
5
16
|
// Ideally the ChainFetcher name suits this better
|
|
6
17
|
// But currently the ChainFetcher module is immutable
|
|
7
18
|
// and handles both processing and fetching.
|
|
8
19
|
// So this module is to encapsulate the fetching logic only
|
|
9
20
|
// with a mutable state for easier reasoning and testing.
|
|
10
21
|
type t = {
|
|
11
|
-
|
|
22
|
+
sourcesState: array<sourceState>,
|
|
12
23
|
mutable statusStart: Hrtime.timeRef,
|
|
13
24
|
mutable status: sourceManagerStatus,
|
|
14
25
|
maxPartitionConcurrency: int,
|
|
15
|
-
|
|
26
|
+
newBlockStallTimeout: int,
|
|
27
|
+
newBlockStallTimeoutLive: int,
|
|
16
28
|
stalledPollingInterval: int,
|
|
17
29
|
getHeightRetryInterval: (~retry: int) => int,
|
|
18
30
|
mutable activeSource: Source.t,
|
|
19
31
|
mutable waitingForNewBlockStateId: option<int>,
|
|
20
32
|
// Should take into consideration partitions fetching for previous states (before rollback)
|
|
21
33
|
mutable fetchingPartitionsCount: int,
|
|
34
|
+
recoveryTimeout: float,
|
|
35
|
+
mutable hasLive: bool,
|
|
22
36
|
}
|
|
23
37
|
|
|
24
38
|
let getActiveSource = sourceManager => sourceManager.activeSource
|
|
25
39
|
|
|
40
|
+
type sourceRole = Primary | Secondary
|
|
41
|
+
|
|
42
|
+
// Determines whether a source is Primary or Secondary given the current mode.
|
|
43
|
+
// isLive=false (backfill): Sync=Primary, Fallback=Secondary, Live=ignored (None).
|
|
44
|
+
// isLive=true with hasLive: Live=Primary, Sync+Fallback=Secondary.
|
|
45
|
+
// isLive=true without hasLive: Sync=Primary, Fallback=Secondary.
|
|
46
|
+
let getSourceRole = (~sourceFor: Source.sourceFor, ~isLive, ~hasLive) =>
|
|
47
|
+
switch (isLive, sourceFor) {
|
|
48
|
+
| (false, Sync) => Some(Primary)
|
|
49
|
+
| (false, Fallback) => Some(Secondary)
|
|
50
|
+
| (false, Live) => None
|
|
51
|
+
| (true, Live) => Some(Primary)
|
|
52
|
+
| (true, Sync) => hasLive ? Some(Secondary) : Some(Primary)
|
|
53
|
+
| (true, Fallback) => Some(Secondary)
|
|
54
|
+
}
|
|
55
|
+
|
|
26
56
|
let makeGetHeightRetryInterval = (
|
|
27
57
|
~initialRetryInterval,
|
|
28
58
|
~backoffMultiplicative,
|
|
@@ -41,17 +71,23 @@ let makeGetHeightRetryInterval = (
|
|
|
41
71
|
let make = (
|
|
42
72
|
~sources: array<Source.t>,
|
|
43
73
|
~maxPartitionConcurrency,
|
|
44
|
-
~
|
|
74
|
+
~isLive,
|
|
75
|
+
~newBlockStallTimeout=60_000,
|
|
76
|
+
~newBlockStallTimeoutLive=20_000,
|
|
45
77
|
~stalledPollingInterval=5_000,
|
|
78
|
+
~recoveryTimeout=60_000.0,
|
|
46
79
|
~getHeightRetryInterval=makeGetHeightRetryInterval(
|
|
47
80
|
~initialRetryInterval=1000,
|
|
48
81
|
~backoffMultiplicative=2,
|
|
49
82
|
~maxRetryInterval=60_000,
|
|
50
83
|
),
|
|
51
84
|
) => {
|
|
52
|
-
let
|
|
53
|
-
|
|
85
|
+
let hasLive = sources->Js.Array2.some(s => s.sourceFor === Live)
|
|
86
|
+
let initialActiveSource = switch sources->Js.Array2.find(source =>
|
|
87
|
+
getSourceRole(~sourceFor=source.sourceFor, ~isLive, ~hasLive) === Some(Primary)
|
|
88
|
+
) {
|
|
54
89
|
| Some(source) => source
|
|
90
|
+
| None => Js.Exn.raiseError("Invalid configuration, no data-source for historical sync provided")
|
|
55
91
|
}
|
|
56
92
|
Prometheus.IndexingMaxConcurrency.set(
|
|
57
93
|
~maxConcurrency=maxPartitionConcurrency,
|
|
@@ -63,15 +99,25 @@ let make = (
|
|
|
63
99
|
)
|
|
64
100
|
{
|
|
65
101
|
maxPartitionConcurrency,
|
|
66
|
-
|
|
102
|
+
sourcesState: sources->Array.map(source => {
|
|
103
|
+
source,
|
|
104
|
+
knownHeight: 0,
|
|
105
|
+
unsubscribe: None,
|
|
106
|
+
pendingHeightResolvers: [],
|
|
107
|
+
disabled: false,
|
|
108
|
+
lastFailedAt: None,
|
|
109
|
+
}),
|
|
67
110
|
activeSource: initialActiveSource,
|
|
68
111
|
waitingForNewBlockStateId: None,
|
|
69
112
|
fetchingPartitionsCount: 0,
|
|
70
|
-
|
|
113
|
+
newBlockStallTimeout,
|
|
114
|
+
newBlockStallTimeoutLive,
|
|
71
115
|
stalledPollingInterval,
|
|
72
116
|
getHeightRetryInterval,
|
|
117
|
+
recoveryTimeout,
|
|
73
118
|
statusStart: Hrtime.makeTimer(),
|
|
74
119
|
status: Idle,
|
|
120
|
+
hasLive,
|
|
75
121
|
}
|
|
76
122
|
}
|
|
77
123
|
|
|
@@ -81,9 +127,9 @@ let trackNewStatus = (sourceManager: t, ~newStatus) => {
|
|
|
81
127
|
| WaitingForNewBlock => Prometheus.IndexingSourceWaitingTime.counter
|
|
82
128
|
| Querieng => Prometheus.IndexingQueryTime.counter
|
|
83
129
|
}
|
|
84
|
-
promCounter->Prometheus.SafeCounter.
|
|
130
|
+
promCounter->Prometheus.SafeCounter.handleFloat(
|
|
85
131
|
~labels=sourceManager.activeSource.chain->ChainMap.Chain.toChainId,
|
|
86
|
-
~value=sourceManager.statusStart->Hrtime.timeSince->Hrtime.
|
|
132
|
+
~value=sourceManager.statusStart->Hrtime.timeSince->Hrtime.toSecondsFloat,
|
|
87
133
|
)
|
|
88
134
|
sourceManager.statusStart = Hrtime.makeTimer()
|
|
89
135
|
sourceManager.status = newStatus
|
|
@@ -92,7 +138,6 @@ let trackNewStatus = (sourceManager: t, ~newStatus) => {
|
|
|
92
138
|
let fetchNext = async (
|
|
93
139
|
sourceManager: t,
|
|
94
140
|
~fetchState: FetchState.t,
|
|
95
|
-
~currentBlockHeight,
|
|
96
141
|
~executeQuery,
|
|
97
142
|
~waitForNewBlock,
|
|
98
143
|
~onNewBlock,
|
|
@@ -100,13 +145,13 @@ let fetchNext = async (
|
|
|
100
145
|
) => {
|
|
101
146
|
let {maxPartitionConcurrency} = sourceManager
|
|
102
147
|
|
|
103
|
-
|
|
148
|
+
let nextQuery = fetchState->FetchState.getNextQuery(
|
|
104
149
|
~concurrencyLimit={
|
|
105
150
|
maxPartitionConcurrency - sourceManager.fetchingPartitionsCount
|
|
106
151
|
},
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
switch nextQuery {
|
|
110
155
|
| ReachedMaxConcurrency
|
|
111
156
|
| NothingToQuery => ()
|
|
112
157
|
| WaitingForNewBlock =>
|
|
@@ -116,19 +161,19 @@ let fetchNext = async (
|
|
|
116
161
|
| None =>
|
|
117
162
|
sourceManager->trackNewStatus(~newStatus=WaitingForNewBlock)
|
|
118
163
|
sourceManager.waitingForNewBlockStateId = Some(stateId)
|
|
119
|
-
let
|
|
164
|
+
let knownHeight = await waitForNewBlock(~knownHeight=fetchState.knownHeight)
|
|
120
165
|
switch sourceManager.waitingForNewBlockStateId {
|
|
121
166
|
| Some(waitingStateId) if waitingStateId === stateId => {
|
|
122
167
|
sourceManager->trackNewStatus(~newStatus=Idle)
|
|
123
168
|
sourceManager.waitingForNewBlockStateId = None
|
|
124
|
-
onNewBlock(~
|
|
169
|
+
onNewBlock(~knownHeight)
|
|
125
170
|
}
|
|
126
171
|
| Some(_) // Don't reset it if we are waiting for another state
|
|
127
172
|
| None => ()
|
|
128
173
|
}
|
|
129
174
|
}
|
|
130
175
|
| Ready(queries) => {
|
|
131
|
-
fetchState->FetchState.startFetchingQueries(~queries
|
|
176
|
+
fetchState->FetchState.startFetchingQueries(~queries)
|
|
132
177
|
sourceManager.fetchingPartitionsCount =
|
|
133
178
|
sourceManager.fetchingPartitionsCount + queries->Array.length
|
|
134
179
|
Prometheus.IndexingConcurrency.set(
|
|
@@ -159,134 +204,245 @@ let fetchNext = async (
|
|
|
159
204
|
|
|
160
205
|
type status = Active | Stalled | Done
|
|
161
206
|
|
|
207
|
+
let disableSource = (sourceManager: t, sourceState: sourceState) => {
|
|
208
|
+
if !sourceState.disabled {
|
|
209
|
+
sourceState.disabled = true
|
|
210
|
+
switch sourceState.unsubscribe {
|
|
211
|
+
| Some(unsubscribe) => unsubscribe()
|
|
212
|
+
| None => ()
|
|
213
|
+
}
|
|
214
|
+
if sourceState.source.sourceFor === Live {
|
|
215
|
+
// Only clear hasLive if no other non-disabled Live sources remain
|
|
216
|
+
let hasOtherLive =
|
|
217
|
+
sourceManager.sourcesState->Js.Array2.some(s =>
|
|
218
|
+
s !== sourceState && !s.disabled && s.source.sourceFor === Live
|
|
219
|
+
)
|
|
220
|
+
sourceManager.hasLive = hasOtherLive
|
|
221
|
+
}
|
|
222
|
+
true
|
|
223
|
+
} else {
|
|
224
|
+
false
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
162
228
|
let getSourceNewHeight = async (
|
|
163
229
|
sourceManager,
|
|
164
|
-
~
|
|
165
|
-
~
|
|
230
|
+
~sourceState: sourceState,
|
|
231
|
+
~knownHeight,
|
|
166
232
|
~status: ref<status>,
|
|
167
233
|
~logger,
|
|
168
234
|
) => {
|
|
169
|
-
let
|
|
235
|
+
let source = sourceState.source
|
|
236
|
+
let initialHeight = sourceState.knownHeight
|
|
237
|
+
let newHeight = ref(initialHeight)
|
|
170
238
|
let retry = ref(0)
|
|
171
239
|
|
|
172
|
-
while newHeight.contents <=
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
240
|
+
while newHeight.contents <= knownHeight && status.contents !== Done {
|
|
241
|
+
// If subscription exists, wait for next height event
|
|
242
|
+
switch sourceState.unsubscribe {
|
|
243
|
+
| Some(_) =>
|
|
244
|
+
let height = await Promise.make((resolve, _reject) => {
|
|
245
|
+
sourceState.pendingHeightResolvers->Array.push(resolve)
|
|
178
246
|
})
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
247
|
+
|
|
248
|
+
// Only accept heights greater than initialHeight
|
|
249
|
+
if height > initialHeight {
|
|
250
|
+
newHeight := height
|
|
251
|
+
}
|
|
252
|
+
| None =>
|
|
253
|
+
// No subscription, use REST polling
|
|
254
|
+
try {
|
|
255
|
+
let height = await source.getHeightOrThrow()
|
|
256
|
+
|
|
257
|
+
newHeight := height
|
|
258
|
+
if height <= knownHeight {
|
|
259
|
+
retry := 0
|
|
260
|
+
|
|
261
|
+
// If createHeightSubscription is available and height hasn't changed,
|
|
262
|
+
// create subscription instead of polling
|
|
263
|
+
switch source.createHeightSubscription {
|
|
264
|
+
| Some(createSubscription) =>
|
|
265
|
+
let unsubscribe = createSubscription(~onHeight=newHeight => {
|
|
266
|
+
sourceState.knownHeight = newHeight
|
|
267
|
+
// Resolve all pending height resolvers
|
|
268
|
+
let resolvers = sourceState.pendingHeightResolvers
|
|
269
|
+
sourceState.pendingHeightResolvers = []
|
|
270
|
+
resolvers->Array.forEach(resolve => resolve(newHeight))
|
|
271
|
+
})
|
|
272
|
+
sourceState.unsubscribe = Some(unsubscribe)
|
|
273
|
+
| None =>
|
|
274
|
+
// Slowdown polling when the chain isn't progressing
|
|
275
|
+
let pollingInterval = if status.contents === Stalled {
|
|
276
|
+
sourceManager.stalledPollingInterval
|
|
277
|
+
} else {
|
|
278
|
+
source.pollingInterval
|
|
279
|
+
}
|
|
280
|
+
await Utils.delay(pollingInterval)
|
|
281
|
+
}
|
|
190
282
|
}
|
|
191
|
-
|
|
283
|
+
} catch {
|
|
284
|
+
| exn =>
|
|
285
|
+
let retryInterval = sourceManager.getHeightRetryInterval(~retry=retry.contents)
|
|
286
|
+
logger->Logging.childTrace({
|
|
287
|
+
"msg": `Height retrieval from ${source.name} source failed. Retrying in ${retryInterval->Int.toString}ms.`,
|
|
288
|
+
"source": source.name,
|
|
289
|
+
"err": exn->Utils.prettifyExn,
|
|
290
|
+
})
|
|
291
|
+
retry := retry.contents + 1
|
|
292
|
+
await Utils.delay(retryInterval)
|
|
192
293
|
}
|
|
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
294
|
}
|
|
204
295
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
296
|
+
|
|
297
|
+
// Update Prometheus only if height increased
|
|
298
|
+
if newHeight.contents > initialHeight {
|
|
299
|
+
Prometheus.SourceHeight.set(
|
|
300
|
+
~sourceName=source.name,
|
|
301
|
+
~chainId=source.chain->ChainMap.Chain.toChainId,
|
|
302
|
+
~blockNumber=newHeight.contents,
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
|
|
210
306
|
newHeight.contents
|
|
211
307
|
}
|
|
212
308
|
|
|
309
|
+
let compareByOldestFailure = (a: sourceState, b: sourceState) =>
|
|
310
|
+
switch (a.lastFailedAt, b.lastFailedAt) {
|
|
311
|
+
| (None, Some(_)) => -1
|
|
312
|
+
| (Some(_), None) => 1
|
|
313
|
+
| (Some(a), Some(b)) => a < b ? -1 : a > b ? 1 : 0
|
|
314
|
+
| (None, None) => 0
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Priority: working primaries > working secondaries > all primaries.
|
|
318
|
+
let getNextSources = (sourceManager, ~isLive, ~excludedSources=?) => {
|
|
319
|
+
let now = Js.Date.now()
|
|
320
|
+
let workingPrimarySources = []
|
|
321
|
+
let allPrimarySources = []
|
|
322
|
+
let workingSecondarySources = []
|
|
323
|
+
for i in 0 to sourceManager.sourcesState->Array.length - 1 {
|
|
324
|
+
let sourceState = sourceManager.sourcesState->Array.getUnsafe(i)
|
|
325
|
+
if !sourceState.disabled {
|
|
326
|
+
let isExcluded = switch excludedSources {
|
|
327
|
+
| Some(set) => set->Utils.Set.has(sourceState)
|
|
328
|
+
| None => false
|
|
329
|
+
}
|
|
330
|
+
if !isExcluded {
|
|
331
|
+
let isWorking = switch sourceState.lastFailedAt {
|
|
332
|
+
| Some(failedAt) => now -. failedAt >= sourceManager.recoveryTimeout
|
|
333
|
+
| None => true
|
|
334
|
+
}
|
|
335
|
+
switch getSourceRole(
|
|
336
|
+
~sourceFor=sourceState.source.sourceFor,
|
|
337
|
+
~isLive,
|
|
338
|
+
~hasLive=sourceManager.hasLive,
|
|
339
|
+
) {
|
|
340
|
+
| Some(Primary) =>
|
|
341
|
+
allPrimarySources->Array.push(sourceState)
|
|
342
|
+
if isWorking {
|
|
343
|
+
workingPrimarySources->Array.push(sourceState)
|
|
344
|
+
}
|
|
345
|
+
| Some(Secondary) if isWorking => workingSecondarySources->Array.push(sourceState)
|
|
346
|
+
| _ => ()
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if workingPrimarySources->Array.length > 0 {
|
|
352
|
+
workingPrimarySources
|
|
353
|
+
} else if workingSecondarySources->Array.length > 0 {
|
|
354
|
+
workingSecondarySources
|
|
355
|
+
} else {
|
|
356
|
+
// All primaries in recovery — sort by oldest lastFailedAt (closest to recovery first)
|
|
357
|
+
allPrimarySources->Js.Array2.sortInPlaceWith(compareByOldestFailure)
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Single source selection from getNextSources.
|
|
362
|
+
// Prefers activeSource if it's in the candidates. Fast path: check first item.
|
|
363
|
+
let getNextSource = (sourceManager, ~isLive, ~excludedSources=?) => {
|
|
364
|
+
let sources = sourceManager->getNextSources(~isLive, ~excludedSources?)
|
|
365
|
+
switch sources->Array.get(0) {
|
|
366
|
+
| None => None
|
|
367
|
+
| Some(first) if first.source === sourceManager.activeSource => Some(first)
|
|
368
|
+
| _ =>
|
|
369
|
+
switch sources->Js.Array2.find(s => s.source === sourceManager.activeSource) {
|
|
370
|
+
| Some(_) as result => result
|
|
371
|
+
| None => sources->Array.get(0)
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
213
376
|
// Polls for a block height greater than the given block number to ensure a new block is available for indexing.
|
|
214
|
-
let waitForNewBlock = async (sourceManager: t, ~
|
|
215
|
-
let {
|
|
377
|
+
let waitForNewBlock = async (sourceManager: t, ~knownHeight, ~isLive) => {
|
|
378
|
+
let {sourcesState} = sourceManager
|
|
216
379
|
|
|
217
380
|
let logger = Logging.createChild(
|
|
218
381
|
~params={
|
|
219
382
|
"chainId": sourceManager.activeSource.chain->ChainMap.Chain.toChainId,
|
|
220
|
-
"
|
|
383
|
+
"knownHeight": knownHeight,
|
|
221
384
|
},
|
|
222
385
|
)
|
|
223
386
|
logger->Logging.childTrace("Initiating check for new blocks.")
|
|
224
387
|
|
|
225
|
-
|
|
226
|
-
// (currentBlockHeight > 0 means we've fetched at least one batch)
|
|
227
|
-
// This prevents Live RPC from winning the initial height race and
|
|
228
|
-
// becoming activeSource, which would bypass HyperSync's smart block detection
|
|
229
|
-
let isInitialHeightFetch = currentBlockHeight === 0
|
|
230
|
-
|
|
231
|
-
let syncSources = []
|
|
232
|
-
let fallbackSources = []
|
|
233
|
-
sources->Utils.Set.forEach(source => {
|
|
234
|
-
if (
|
|
235
|
-
source.sourceFor === Sync ||
|
|
236
|
-
// Include Live sources only after initial sync has started
|
|
237
|
-
// Live sources are optimized for real-time indexing with lower latency
|
|
238
|
-
(source.sourceFor === Live && !isInitialHeightFetch) ||
|
|
239
|
-
// Even if the active source is a fallback, still include
|
|
240
|
-
// it to the list. So we don't wait for a timeout again
|
|
241
|
-
// if all main sync sources are still not valid
|
|
242
|
-
source === sourceManager.activeSource
|
|
243
|
-
) {
|
|
244
|
-
syncSources->Array.push(source)
|
|
245
|
-
} else {
|
|
246
|
-
fallbackSources->Array.push(source)
|
|
247
|
-
}
|
|
248
|
-
})
|
|
388
|
+
let mainSources = sourceManager->getNextSources(~isLive)
|
|
249
389
|
|
|
250
390
|
let status = ref(Active)
|
|
251
391
|
|
|
392
|
+
let stallTimeout = if isLive {
|
|
393
|
+
sourceManager.newBlockStallTimeoutLive
|
|
394
|
+
} else {
|
|
395
|
+
sourceManager.newBlockStallTimeout
|
|
396
|
+
}
|
|
397
|
+
|
|
252
398
|
let (source, newBlockHeight) = await Promise.race(
|
|
253
|
-
|
|
254
|
-
->Array.map(async
|
|
399
|
+
mainSources
|
|
400
|
+
->Array.map(async sourceState => {
|
|
255
401
|
(
|
|
256
|
-
source,
|
|
257
|
-
await sourceManager->getSourceNewHeight(~
|
|
402
|
+
sourceState.source,
|
|
403
|
+
await sourceManager->getSourceNewHeight(~sourceState, ~knownHeight, ~status, ~logger),
|
|
258
404
|
)
|
|
259
405
|
})
|
|
260
406
|
->Array.concat([
|
|
261
|
-
Utils.delay(
|
|
407
|
+
Utils.delay(stallTimeout)->Promise.then(() => {
|
|
408
|
+
// Build fallback: sources not in mainSources with a valid role, even with recent lastFailedAt
|
|
409
|
+
let fallbackSources = []
|
|
410
|
+
sourcesState->Array.forEach(sourceState => {
|
|
411
|
+
if (
|
|
412
|
+
!(mainSources->Js.Array2.includes(sourceState)) &&
|
|
413
|
+
getSourceRole(
|
|
414
|
+
~sourceFor=sourceState.source.sourceFor,
|
|
415
|
+
~isLive,
|
|
416
|
+
~hasLive=sourceManager.hasLive,
|
|
417
|
+
)->Option.isSome
|
|
418
|
+
) {
|
|
419
|
+
fallbackSources->Array.push(sourceState)
|
|
420
|
+
}
|
|
421
|
+
})
|
|
422
|
+
|
|
262
423
|
if status.contents !== Done {
|
|
263
424
|
status := Stalled
|
|
264
425
|
|
|
265
426
|
switch fallbackSources {
|
|
266
427
|
| [] =>
|
|
267
428
|
logger->Logging.childWarn(
|
|
268
|
-
`No new blocks detected within ${(
|
|
429
|
+
`No new blocks detected within ${(stallTimeout / 1000)
|
|
269
430
|
->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`,
|
|
270
431
|
)
|
|
271
432
|
| _ =>
|
|
272
433
|
logger->Logging.childWarn(
|
|
273
|
-
`No new blocks detected within ${(
|
|
274
|
-
->Int.toString}s. Continuing polling with
|
|
434
|
+
`No new blocks detected within ${(stallTimeout / 1000)
|
|
435
|
+
->Int.toString}s. Continuing polling with secondary RPC sources from the configuration.`,
|
|
275
436
|
)
|
|
276
437
|
}
|
|
277
438
|
}
|
|
278
439
|
// Promise.race will be forever pending if fallbackSources is empty
|
|
279
440
|
// which is good for this use case
|
|
280
441
|
Promise.race(
|
|
281
|
-
fallbackSources->Array.map(async
|
|
442
|
+
fallbackSources->Array.map(async sourceState => {
|
|
282
443
|
(
|
|
283
|
-
source,
|
|
284
|
-
await sourceManager->getSourceNewHeight(
|
|
285
|
-
~source,
|
|
286
|
-
~currentBlockHeight,
|
|
287
|
-
~status,
|
|
288
|
-
~logger,
|
|
289
|
-
),
|
|
444
|
+
sourceState.source,
|
|
445
|
+
await sourceManager->getSourceNewHeight(~sourceState, ~knownHeight, ~status, ~logger),
|
|
290
446
|
)
|
|
291
447
|
}),
|
|
292
448
|
)
|
|
@@ -296,7 +452,7 @@ let waitForNewBlock = async (sourceManager: t, ~currentBlockHeight) => {
|
|
|
296
452
|
|
|
297
453
|
sourceManager.activeSource = source
|
|
298
454
|
|
|
299
|
-
// Show a higher level log if we displayed a warning/error after
|
|
455
|
+
// Show a higher level log if we displayed a warning/error after newBlockStallTimeout
|
|
300
456
|
let log = status.contents === Stalled ? Logging.childInfo : Logging.childTrace
|
|
301
457
|
logger->log({
|
|
302
458
|
"msg": `New blocks successfully found.`,
|
|
@@ -309,63 +465,43 @@ let waitForNewBlock = async (sourceManager: t, ~currentBlockHeight) => {
|
|
|
309
465
|
newBlockHeight
|
|
310
466
|
}
|
|
311
467
|
|
|
312
|
-
let
|
|
313
|
-
|
|
314
|
-
// This is needed to include the Fallback source to rotation
|
|
315
|
-
~initialSource,
|
|
316
|
-
~currentSource,
|
|
317
|
-
// After multiple failures start returning fallback sources as well
|
|
318
|
-
// But don't try it when main sync sources fail because of invalid configuration
|
|
319
|
-
// note: The logic might be changed in the future
|
|
320
|
-
~attemptFallbacks=false,
|
|
321
|
-
) => {
|
|
322
|
-
let before = []
|
|
323
|
-
let after = []
|
|
324
|
-
|
|
325
|
-
let hasActive = ref(false)
|
|
326
|
-
|
|
327
|
-
sourceManager.sources->Utils.Set.forEach(source => {
|
|
328
|
-
if source === currentSource {
|
|
329
|
-
hasActive := true
|
|
330
|
-
} else if (
|
|
331
|
-
switch source.sourceFor {
|
|
332
|
-
| Sync => true
|
|
333
|
-
// Live sources should NOT be used for historical sync rotation
|
|
334
|
-
// They are only meant for real-time indexing once synced
|
|
335
|
-
| Live | Fallback => attemptFallbacks || source === initialSource
|
|
336
|
-
}
|
|
337
|
-
) {
|
|
338
|
-
(hasActive.contents ? after : before)->Array.push(source)
|
|
339
|
-
}
|
|
340
|
-
})
|
|
468
|
+
let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~knownHeight, ~isLive) => {
|
|
469
|
+
let noSourcesError = "The indexer doesn't have data-sources which can continue fetching. Please, check the error logs or reach out to the Envio team."
|
|
341
470
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
| None =>
|
|
345
|
-
switch before->Array.get(0) {
|
|
346
|
-
| Some(s) => s
|
|
347
|
-
| None => currentSource
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
}
|
|
471
|
+
// Sources where the query is impossible — lazily allocated, excluded for the duration of this query
|
|
472
|
+
let excludedSourcesRef = ref(None)
|
|
351
473
|
|
|
352
|
-
let
|
|
353
|
-
let toBlockRef = ref(
|
|
354
|
-
switch query.target {
|
|
355
|
-
| Head => None
|
|
356
|
-
| EndBlock({toBlock})
|
|
357
|
-
| Merge({toBlock}) =>
|
|
358
|
-
Some(toBlock)
|
|
359
|
-
},
|
|
360
|
-
)
|
|
474
|
+
let toBlockRef = ref(query.toBlock)
|
|
361
475
|
let responseRef = ref(None)
|
|
362
476
|
let retryRef = ref(0)
|
|
363
|
-
let initialSource = sourceManager.activeSource
|
|
364
|
-
let sourceRef = ref(initialSource)
|
|
365
|
-
let shouldUpdateActiveSource = ref(false)
|
|
366
477
|
|
|
367
478
|
while responseRef.contents->Option.isNone {
|
|
368
|
-
|
|
479
|
+
// Select the best source at the start of every iteration
|
|
480
|
+
let sourceState = switch sourceManager->getNextSource(
|
|
481
|
+
~isLive,
|
|
482
|
+
~excludedSources=?excludedSourcesRef.contents,
|
|
483
|
+
) {
|
|
484
|
+
| Some(s) =>
|
|
485
|
+
if s.source !== sourceManager.activeSource {
|
|
486
|
+
let logger = Logging.createChild(
|
|
487
|
+
~params={"chainId": sourceManager.activeSource.chain->ChainMap.Chain.toChainId},
|
|
488
|
+
)
|
|
489
|
+
logger->Logging.childInfo({
|
|
490
|
+
"msg": "Switching data-source",
|
|
491
|
+
"source": s.source.name,
|
|
492
|
+
"previousSource": sourceManager.activeSource.name,
|
|
493
|
+
"fromBlock": query.fromBlock,
|
|
494
|
+
})
|
|
495
|
+
}
|
|
496
|
+
s
|
|
497
|
+
| None =>
|
|
498
|
+
let logger = Logging.createChild(
|
|
499
|
+
~params={"chainId": sourceManager.activeSource.chain->ChainMap.Chain.toChainId},
|
|
500
|
+
)
|
|
501
|
+
%raw(`null`)->ErrorHandling.mkLogAndRaise(~logger, ~msg=noSourcesError)
|
|
502
|
+
}
|
|
503
|
+
sourceManager.activeSource = sourceState.source
|
|
504
|
+
let source = sourceState.source
|
|
369
505
|
let toBlock = toBlockRef.contents
|
|
370
506
|
let retry = retryRef.contents
|
|
371
507
|
|
|
@@ -389,7 +525,7 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~currentBl
|
|
|
389
525
|
~addressesByContractName=query.addressesByContractName,
|
|
390
526
|
~indexingContracts=query.indexingContracts,
|
|
391
527
|
~partitionId=query.partitionId,
|
|
392
|
-
~
|
|
528
|
+
~knownHeight,
|
|
393
529
|
~selection=query.selection,
|
|
394
530
|
~retry,
|
|
395
531
|
~logger,
|
|
@@ -400,21 +536,20 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~currentBl
|
|
|
400
536
|
"numEvents": response.parsedQueueItems->Array.length,
|
|
401
537
|
"stats": response.stats,
|
|
402
538
|
})
|
|
539
|
+
sourceState.lastFailedAt = None
|
|
403
540
|
responseRef := Some(response)
|
|
404
541
|
} catch {
|
|
405
542
|
| Source.GetItemsError(error) =>
|
|
406
543
|
switch error {
|
|
407
544
|
| UnsupportedSelection(_)
|
|
408
545
|
| FailedGettingFieldSelection(_) => {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
// from sourceManager so it's not attempted anymore
|
|
413
|
-
let notAlreadyDeleted = sourceManager.sources->Utils.Set.delete(source)
|
|
546
|
+
// These errors are impossible to recover, so we disable the source
|
|
547
|
+
// so it's not attempted anymore
|
|
548
|
+
let notAlreadyDisabled = sourceManager->disableSource(sourceState)
|
|
414
549
|
|
|
415
550
|
// In case there are multiple partitions
|
|
416
551
|
// failing at the same time. Log only once
|
|
417
|
-
if
|
|
552
|
+
if notAlreadyDisabled {
|
|
418
553
|
switch error {
|
|
419
554
|
| UnsupportedSelection({message}) => logger->Logging.childError(message)
|
|
420
555
|
| FailedGettingFieldSelection({exn, message, blockNumber, logIndex}) =>
|
|
@@ -428,20 +563,7 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~currentBl
|
|
|
428
563
|
}
|
|
429
564
|
}
|
|
430
565
|
|
|
431
|
-
|
|
432
|
-
%raw(`null`)->ErrorHandling.mkLogAndRaise(
|
|
433
|
-
~logger,
|
|
434
|
-
~msg="The indexer doesn't have data-sources which can continue fetching. Please, check the error logs or reach out to the Envio team.",
|
|
435
|
-
)
|
|
436
|
-
} else {
|
|
437
|
-
logger->Logging.childInfo({
|
|
438
|
-
"msg": "Switching to another data-source",
|
|
439
|
-
"source": nextSource.name,
|
|
440
|
-
})
|
|
441
|
-
sourceRef := nextSource
|
|
442
|
-
shouldUpdateActiveSource := true
|
|
443
|
-
retryRef := 0
|
|
444
|
-
}
|
|
566
|
+
retryRef := 0
|
|
445
567
|
}
|
|
446
568
|
| FailedGettingItems({attemptedToBlock, retry: WithSuggestedToBlock({toBlock})}) =>
|
|
447
569
|
logger->Logging.childTrace({
|
|
@@ -452,58 +574,24 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~currentBl
|
|
|
452
574
|
toBlockRef := Some(toBlock)
|
|
453
575
|
retryRef := 0
|
|
454
576
|
| FailedGettingItems({exn, attemptedToBlock, retry: ImpossibleForTheQuery({message})}) =>
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
)
|
|
461
|
-
|
|
462
|
-
|
|
577
|
+
// Don't set lastFailedAt — the source isn't broken, the query just can't work on it
|
|
578
|
+
let excludedSources = switch excludedSourcesRef.contents {
|
|
579
|
+
| Some(s) => s
|
|
580
|
+
| None =>
|
|
581
|
+
let s = Utils.Set.make()
|
|
582
|
+
excludedSourcesRef := Some(s)
|
|
583
|
+
s
|
|
584
|
+
}
|
|
585
|
+
excludedSources->Utils.Set.add(sourceState)->ignore
|
|
463
586
|
|
|
464
587
|
logger->Logging.childWarn({
|
|
465
|
-
"msg": message ++
|
|
588
|
+
"msg": message ++ " - Attempting another source",
|
|
466
589
|
"toBlock": attemptedToBlock,
|
|
467
590
|
"err": exn->Utils.prettifyExn,
|
|
468
591
|
})
|
|
469
|
-
|
|
470
|
-
if !hasAnotherSource {
|
|
471
|
-
%raw(`null`)->ErrorHandling.mkLogAndRaise(
|
|
472
|
-
~logger,
|
|
473
|
-
~msg="The indexer doesn't have data-sources which can continue fetching. Please, check the error logs or reach out to the Envio team.",
|
|
474
|
-
)
|
|
475
|
-
} else {
|
|
476
|
-
sourceRef := nextSource
|
|
477
|
-
shouldUpdateActiveSource := false
|
|
478
|
-
retryRef := 0
|
|
479
|
-
}
|
|
592
|
+
retryRef := 0
|
|
480
593
|
|
|
481
594
|
| FailedGettingItems({exn, attemptedToBlock, retry: WithBackoff({message, backoffMillis})}) =>
|
|
482
|
-
// Starting from the 11th failure (retry=10)
|
|
483
|
-
// include fallback sources for switch
|
|
484
|
-
// (previously it would consider only sync sources or the initial one)
|
|
485
|
-
// This is a little bit tricky to find the right number,
|
|
486
|
-
// because meaning between RPC and HyperSync is different for the error
|
|
487
|
-
// but since Fallback was initially designed to be used only for height check
|
|
488
|
-
// just keep the value high
|
|
489
|
-
let attemptFallbacks = retry >= 10
|
|
490
|
-
|
|
491
|
-
let nextSource = switch retry {
|
|
492
|
-
// Don't attempt a switch on first two failure
|
|
493
|
-
| 0 | 1 => source
|
|
494
|
-
| _ =>
|
|
495
|
-
// Then try to switch every second failure
|
|
496
|
-
if retry->mod(2) === 0 {
|
|
497
|
-
sourceManager->getNextSyncSource(
|
|
498
|
-
~initialSource,
|
|
499
|
-
~attemptFallbacks,
|
|
500
|
-
~currentSource=source,
|
|
501
|
-
)
|
|
502
|
-
} else {
|
|
503
|
-
source
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
|
|
507
595
|
// Start displaying warnings after 4 failures
|
|
508
596
|
let log = retry >= 4 ? Logging.childWarn : Logging.childTrace
|
|
509
597
|
logger->log({
|
|
@@ -514,14 +602,30 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~currentBl
|
|
|
514
602
|
"err": exn->Utils.prettifyExn,
|
|
515
603
|
})
|
|
516
604
|
|
|
517
|
-
let shouldSwitch =
|
|
605
|
+
let shouldSwitch = switch retry {
|
|
606
|
+
// Don't attempt a switch on first two failures
|
|
607
|
+
| 0 | 1 => false
|
|
608
|
+
// Then try to switch every second failure
|
|
609
|
+
| _ => retry->mod(2) === 0
|
|
610
|
+
}
|
|
611
|
+
|
|
518
612
|
if shouldSwitch {
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
613
|
+
let now = Js.Date.now()
|
|
614
|
+
sourceState.lastFailedAt = Some(now)
|
|
615
|
+
// Check if there's a working (recovered) source to switch to immediately
|
|
616
|
+
let nextSource =
|
|
617
|
+
sourceManager->getNextSource(~isLive, ~excludedSources=?excludedSourcesRef.contents)
|
|
618
|
+
let hasWorkingAlternative = switch nextSource {
|
|
619
|
+
| Some(s) =>
|
|
620
|
+
switch s.lastFailedAt {
|
|
621
|
+
| None => true
|
|
622
|
+
| Some(failedAt) => now -. failedAt >= sourceManager.recoveryTimeout
|
|
623
|
+
}
|
|
624
|
+
| None => false
|
|
625
|
+
}
|
|
626
|
+
if !hasWorkingAlternative {
|
|
627
|
+
await Utils.delay(Pervasives.min(backoffMillis, 60_000))
|
|
628
|
+
}
|
|
525
629
|
} else {
|
|
526
630
|
await Utils.delay(Pervasives.min(backoffMillis, 60_000))
|
|
527
631
|
}
|
|
@@ -533,9 +637,5 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~currentBl
|
|
|
533
637
|
}
|
|
534
638
|
}
|
|
535
639
|
|
|
536
|
-
if shouldUpdateActiveSource.contents {
|
|
537
|
-
sourceManager.activeSource = sourceRef.contents
|
|
538
|
-
}
|
|
539
|
-
|
|
540
640
|
responseRef.contents->Option.getUnsafe
|
|
541
641
|
}
|