envio 3.0.0-alpha.2 → 3.0.0-alpha.21
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 +578 -1
- package/index.js +4 -0
- package/package.json +47 -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 +725 -25
- package/src/Config.res.mjs +692 -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 +33 -73
- package/src/Env.res.mjs +29 -85
- package/src/Envio.gen.ts +3 -1
- package/src/Envio.res +77 -9
- package/src/Envio.res.mjs +39 -1
- package/src/EventConfigBuilder.res +408 -0
- package/src/EventConfigBuilder.res.mjs +376 -0
- 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 +1224 -0
- package/src/GlobalState.res.mjs +1291 -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/HandlerRegister.resi +30 -0
- 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 +6 -0
- package/src/Internal.res +265 -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 +390 -0
- package/src/Main.res.mjs +341 -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/SimulateItems.res +353 -0
- package/src/SimulateItems.res.mjs +335 -0
- package/src/Sink.res +4 -2
- package/src/Sink.res.mjs +2 -1
- package/src/TableIndices.res +0 -1
- package/src/TestIndexer.res +913 -0
- package/src/TestIndexer.res.mjs +698 -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 +11 -2
- 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 +144 -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 +35 -13
- package/src/sources/HyperFuelSource.res.mjs +26 -16
- package/src/sources/HyperSync.res +61 -60
- package/src/sources/HyperSync.res.mjs +53 -67
- package/src/sources/HyperSync.resi +6 -4
- package/src/sources/HyperSyncClient.res +29 -2
- package/src/sources/HyperSyncClient.res.mjs +9 -0
- package/src/sources/HyperSyncHeightStream.res +76 -118
- package/src/sources/HyperSyncHeightStream.res.mjs +68 -75
- package/src/sources/HyperSyncSource.res +122 -143
- package/src/sources/HyperSyncSource.res.mjs +106 -121
- package/src/sources/Rpc.res +86 -14
- package/src/sources/Rpc.res.mjs +101 -9
- package/src/sources/RpcSource.res +731 -364
- package/src/sources/RpcSource.res.mjs +845 -410
- package/src/sources/RpcWebSocketHeightStream.res +181 -0
- package/src/sources/RpcWebSocketHeightStream.res.mjs +196 -0
- package/src/sources/SimulateSource.res +59 -0
- package/src/sources/SimulateSource.res.mjs +50 -0
- package/src/sources/Source.res +7 -5
- package/src/sources/SourceManager.res +358 -221
- package/src/sources/SourceManager.res.mjs +346 -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/EventRegister.resi +0 -30
- 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,131 +204,279 @@ 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,
|
|
232
|
+
~stallTimeout,
|
|
233
|
+
~isLive,
|
|
166
234
|
~status: ref<status>,
|
|
167
235
|
~logger,
|
|
168
236
|
) => {
|
|
169
|
-
let
|
|
237
|
+
let source = sourceState.source
|
|
238
|
+
let initialHeight = sourceState.knownHeight
|
|
239
|
+
let newHeight = ref(initialHeight)
|
|
170
240
|
let retry = ref(0)
|
|
171
241
|
|
|
172
|
-
while newHeight.contents <=
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
242
|
+
while newHeight.contents <= knownHeight && status.contents !== Done {
|
|
243
|
+
// If subscription exists, wait for next height event
|
|
244
|
+
switch sourceState.unsubscribe {
|
|
245
|
+
| Some(_) =>
|
|
246
|
+
let subscriptionPromise = Promise.make((resolve, _reject) => {
|
|
247
|
+
sourceState.pendingHeightResolvers->Array.push(resolve)
|
|
178
248
|
})
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
let
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
249
|
+
// If subscription goes quiet for half the stall timeout, fall back to REST polling
|
|
250
|
+
let pollingFallback = Utils.delay(stallTimeout / 2)->Promise.then(async () => {
|
|
251
|
+
logger->Logging.childTrace({
|
|
252
|
+
"msg": "onHeight subscription stale, switching to polling fallback",
|
|
253
|
+
"source": source.name,
|
|
254
|
+
"chainId": source.chain->ChainMap.Chain.toChainId,
|
|
255
|
+
})
|
|
256
|
+
let h = ref(initialHeight)
|
|
257
|
+
while h.contents <= knownHeight && !(newHeight.contents > initialHeight) {
|
|
258
|
+
try {
|
|
259
|
+
h := (await source.getHeightOrThrow())
|
|
260
|
+
} catch {
|
|
261
|
+
| _ => ()
|
|
262
|
+
}
|
|
263
|
+
if h.contents <= knownHeight && !(newHeight.contents > initialHeight) {
|
|
264
|
+
await Utils.delay(source.pollingInterval)
|
|
265
|
+
}
|
|
190
266
|
}
|
|
191
|
-
|
|
192
|
-
}
|
|
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,
|
|
267
|
+
h.contents
|
|
200
268
|
})
|
|
201
|
-
|
|
202
|
-
|
|
269
|
+
let height = await Promise.race([subscriptionPromise, pollingFallback])
|
|
270
|
+
|
|
271
|
+
// Only accept heights greater than initialHeight
|
|
272
|
+
if height > initialHeight {
|
|
273
|
+
newHeight := height
|
|
274
|
+
}
|
|
275
|
+
| None =>
|
|
276
|
+
// No subscription, use REST polling
|
|
277
|
+
try {
|
|
278
|
+
let height = await source.getHeightOrThrow()
|
|
279
|
+
|
|
280
|
+
newHeight := height
|
|
281
|
+
if height <= knownHeight {
|
|
282
|
+
retry := 0
|
|
283
|
+
|
|
284
|
+
// If createHeightSubscription is available and height hasn't changed,
|
|
285
|
+
// create subscription instead of polling
|
|
286
|
+
switch source.createHeightSubscription {
|
|
287
|
+
| Some(createSubscription) if isLive =>
|
|
288
|
+
let unsubscribe = createSubscription(~onHeight=newHeight => {
|
|
289
|
+
sourceState.knownHeight = newHeight
|
|
290
|
+
// Resolve all pending height resolvers
|
|
291
|
+
let resolvers = sourceState.pendingHeightResolvers
|
|
292
|
+
sourceState.pendingHeightResolvers = []
|
|
293
|
+
resolvers->Array.forEach(resolve => resolve(newHeight))
|
|
294
|
+
})
|
|
295
|
+
sourceState.unsubscribe = Some(unsubscribe)
|
|
296
|
+
| _ =>
|
|
297
|
+
// Slowdown polling when the chain isn't progressing
|
|
298
|
+
let pollingInterval = if status.contents === Stalled {
|
|
299
|
+
sourceManager.stalledPollingInterval
|
|
300
|
+
} else {
|
|
301
|
+
source.pollingInterval
|
|
302
|
+
}
|
|
303
|
+
await Utils.delay(pollingInterval)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
} catch {
|
|
307
|
+
| exn =>
|
|
308
|
+
let retryInterval = sourceManager.getHeightRetryInterval(~retry=retry.contents)
|
|
309
|
+
logger->Logging.childTrace({
|
|
310
|
+
"msg": `Height retrieval from ${source.name} source failed. Retrying in ${retryInterval->Int.toString}ms.`,
|
|
311
|
+
"source": source.name,
|
|
312
|
+
"err": exn->Utils.prettifyExn,
|
|
313
|
+
})
|
|
314
|
+
retry := retry.contents + 1
|
|
315
|
+
await Utils.delay(retryInterval)
|
|
316
|
+
}
|
|
203
317
|
}
|
|
204
318
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
319
|
+
|
|
320
|
+
// Update Prometheus only if height increased
|
|
321
|
+
if newHeight.contents > initialHeight {
|
|
322
|
+
Prometheus.SourceHeight.set(
|
|
323
|
+
~sourceName=source.name,
|
|
324
|
+
~chainId=source.chain->ChainMap.Chain.toChainId,
|
|
325
|
+
~blockNumber=newHeight.contents,
|
|
326
|
+
)
|
|
327
|
+
}
|
|
328
|
+
|
|
210
329
|
newHeight.contents
|
|
211
330
|
}
|
|
212
331
|
|
|
332
|
+
let compareByOldestFailure = (a: sourceState, b: sourceState) =>
|
|
333
|
+
switch (a.lastFailedAt, b.lastFailedAt) {
|
|
334
|
+
| (None, Some(_)) => -1
|
|
335
|
+
| (Some(_), None) => 1
|
|
336
|
+
| (Some(a), Some(b)) => a < b ? -1 : a > b ? 1 : 0
|
|
337
|
+
| (None, None) => 0
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Priority: working primaries > working secondaries > all primaries.
|
|
341
|
+
let getNextSources = (sourceManager, ~isLive, ~excludedSources=?) => {
|
|
342
|
+
let now = Js.Date.now()
|
|
343
|
+
let workingPrimarySources = []
|
|
344
|
+
let allPrimarySources = []
|
|
345
|
+
let workingSecondarySources = []
|
|
346
|
+
for i in 0 to sourceManager.sourcesState->Array.length - 1 {
|
|
347
|
+
let sourceState = sourceManager.sourcesState->Array.getUnsafe(i)
|
|
348
|
+
if !sourceState.disabled {
|
|
349
|
+
let isExcluded = switch excludedSources {
|
|
350
|
+
| Some(set) => set->Utils.Set.has(sourceState)
|
|
351
|
+
| None => false
|
|
352
|
+
}
|
|
353
|
+
if !isExcluded {
|
|
354
|
+
let isWorking = switch sourceState.lastFailedAt {
|
|
355
|
+
| Some(failedAt) => now -. failedAt >= sourceManager.recoveryTimeout
|
|
356
|
+
| None => true
|
|
357
|
+
}
|
|
358
|
+
switch getSourceRole(
|
|
359
|
+
~sourceFor=sourceState.source.sourceFor,
|
|
360
|
+
~isLive,
|
|
361
|
+
~hasLive=sourceManager.hasLive,
|
|
362
|
+
) {
|
|
363
|
+
| Some(Primary) =>
|
|
364
|
+
allPrimarySources->Array.push(sourceState)
|
|
365
|
+
if isWorking {
|
|
366
|
+
workingPrimarySources->Array.push(sourceState)
|
|
367
|
+
}
|
|
368
|
+
| Some(Secondary) if isWorking => workingSecondarySources->Array.push(sourceState)
|
|
369
|
+
| _ => ()
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if workingPrimarySources->Array.length > 0 {
|
|
375
|
+
workingPrimarySources
|
|
376
|
+
} else if workingSecondarySources->Array.length > 0 {
|
|
377
|
+
workingSecondarySources
|
|
378
|
+
} else {
|
|
379
|
+
// All primaries in recovery — sort by oldest lastFailedAt (closest to recovery first)
|
|
380
|
+
allPrimarySources->Js.Array2.sortInPlaceWith(compareByOldestFailure)
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Single source selection from getNextSources.
|
|
385
|
+
// Prefers activeSource if it's in the candidates. Fast path: check first item.
|
|
386
|
+
let getNextSource = (sourceManager, ~isLive, ~excludedSources=?) => {
|
|
387
|
+
let sources = sourceManager->getNextSources(~isLive, ~excludedSources?)
|
|
388
|
+
switch sources->Array.get(0) {
|
|
389
|
+
| None => None
|
|
390
|
+
| Some(first) if first.source === sourceManager.activeSource => Some(first)
|
|
391
|
+
| _ =>
|
|
392
|
+
switch sources->Js.Array2.find(s => s.source === sourceManager.activeSource) {
|
|
393
|
+
| Some(_) as result => result
|
|
394
|
+
| None => sources->Array.get(0)
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
213
399
|
// 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 {
|
|
400
|
+
let waitForNewBlock = async (sourceManager: t, ~knownHeight, ~isLive) => {
|
|
401
|
+
let {sourcesState} = sourceManager
|
|
216
402
|
|
|
217
403
|
let logger = Logging.createChild(
|
|
218
404
|
~params={
|
|
219
405
|
"chainId": sourceManager.activeSource.chain->ChainMap.Chain.toChainId,
|
|
220
|
-
"
|
|
406
|
+
"knownHeight": knownHeight,
|
|
221
407
|
},
|
|
222
408
|
)
|
|
223
409
|
logger->Logging.childTrace("Initiating check for new blocks.")
|
|
224
410
|
|
|
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
|
-
})
|
|
411
|
+
let mainSources = sourceManager->getNextSources(~isLive)
|
|
249
412
|
|
|
250
413
|
let status = ref(Active)
|
|
251
414
|
|
|
415
|
+
let stallTimeout = if isLive {
|
|
416
|
+
sourceManager.newBlockStallTimeoutLive
|
|
417
|
+
} else {
|
|
418
|
+
sourceManager.newBlockStallTimeout
|
|
419
|
+
}
|
|
420
|
+
|
|
252
421
|
let (source, newBlockHeight) = await Promise.race(
|
|
253
|
-
|
|
254
|
-
->Array.map(async
|
|
422
|
+
mainSources
|
|
423
|
+
->Array.map(async sourceState => {
|
|
255
424
|
(
|
|
256
|
-
source,
|
|
257
|
-
await sourceManager->getSourceNewHeight(
|
|
425
|
+
sourceState.source,
|
|
426
|
+
await sourceManager->getSourceNewHeight(
|
|
427
|
+
~sourceState,
|
|
428
|
+
~knownHeight,
|
|
429
|
+
~stallTimeout,
|
|
430
|
+
~isLive,
|
|
431
|
+
~status,
|
|
432
|
+
~logger,
|
|
433
|
+
),
|
|
258
434
|
)
|
|
259
435
|
})
|
|
260
436
|
->Array.concat([
|
|
261
|
-
Utils.delay(
|
|
437
|
+
Utils.delay(stallTimeout)->Promise.then(() => {
|
|
438
|
+
// Build fallback: sources not in mainSources with a valid role, even with recent lastFailedAt
|
|
439
|
+
let fallbackSources = []
|
|
440
|
+
sourcesState->Array.forEach(sourceState => {
|
|
441
|
+
if (
|
|
442
|
+
!(mainSources->Js.Array2.includes(sourceState)) &&
|
|
443
|
+
getSourceRole(
|
|
444
|
+
~sourceFor=sourceState.source.sourceFor,
|
|
445
|
+
~isLive,
|
|
446
|
+
~hasLive=sourceManager.hasLive,
|
|
447
|
+
)->Option.isSome
|
|
448
|
+
) {
|
|
449
|
+
fallbackSources->Array.push(sourceState)
|
|
450
|
+
}
|
|
451
|
+
})
|
|
452
|
+
|
|
262
453
|
if status.contents !== Done {
|
|
263
454
|
status := Stalled
|
|
264
455
|
|
|
265
456
|
switch fallbackSources {
|
|
266
457
|
| [] =>
|
|
267
458
|
logger->Logging.childWarn(
|
|
268
|
-
`No new blocks detected within ${(
|
|
459
|
+
`No new blocks detected within ${(stallTimeout / 1000)
|
|
269
460
|
->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
461
|
)
|
|
271
462
|
| _ =>
|
|
272
463
|
logger->Logging.childWarn(
|
|
273
|
-
`No new blocks detected within ${(
|
|
274
|
-
->Int.toString}s. Continuing polling with
|
|
464
|
+
`No new blocks detected within ${(stallTimeout / 1000)
|
|
465
|
+
->Int.toString}s. Continuing polling with secondary RPC sources from the configuration.`,
|
|
275
466
|
)
|
|
276
467
|
}
|
|
277
468
|
}
|
|
278
469
|
// Promise.race will be forever pending if fallbackSources is empty
|
|
279
470
|
// which is good for this use case
|
|
280
471
|
Promise.race(
|
|
281
|
-
fallbackSources->Array.map(async
|
|
472
|
+
fallbackSources->Array.map(async sourceState => {
|
|
282
473
|
(
|
|
283
|
-
source,
|
|
474
|
+
sourceState.source,
|
|
284
475
|
await sourceManager->getSourceNewHeight(
|
|
285
|
-
~
|
|
286
|
-
~
|
|
476
|
+
~sourceState,
|
|
477
|
+
~knownHeight,
|
|
478
|
+
~stallTimeout,
|
|
479
|
+
~isLive,
|
|
287
480
|
~status,
|
|
288
481
|
~logger,
|
|
289
482
|
),
|
|
@@ -296,7 +489,7 @@ let waitForNewBlock = async (sourceManager: t, ~currentBlockHeight) => {
|
|
|
296
489
|
|
|
297
490
|
sourceManager.activeSource = source
|
|
298
491
|
|
|
299
|
-
// Show a higher level log if we displayed a warning/error after
|
|
492
|
+
// Show a higher level log if we displayed a warning/error after newBlockStallTimeout
|
|
300
493
|
let log = status.contents === Stalled ? Logging.childInfo : Logging.childTrace
|
|
301
494
|
logger->log({
|
|
302
495
|
"msg": `New blocks successfully found.`,
|
|
@@ -309,63 +502,43 @@ let waitForNewBlock = async (sourceManager: t, ~currentBlockHeight) => {
|
|
|
309
502
|
newBlockHeight
|
|
310
503
|
}
|
|
311
504
|
|
|
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
|
-
})
|
|
505
|
+
let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~knownHeight, ~isLive) => {
|
|
506
|
+
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
507
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
| None =>
|
|
345
|
-
switch before->Array.get(0) {
|
|
346
|
-
| Some(s) => s
|
|
347
|
-
| None => currentSource
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
}
|
|
508
|
+
// Sources where the query is impossible — lazily allocated, excluded for the duration of this query
|
|
509
|
+
let excludedSourcesRef = ref(None)
|
|
351
510
|
|
|
352
|
-
let
|
|
353
|
-
let toBlockRef = ref(
|
|
354
|
-
switch query.target {
|
|
355
|
-
| Head => None
|
|
356
|
-
| EndBlock({toBlock})
|
|
357
|
-
| Merge({toBlock}) =>
|
|
358
|
-
Some(toBlock)
|
|
359
|
-
},
|
|
360
|
-
)
|
|
511
|
+
let toBlockRef = ref(query.toBlock)
|
|
361
512
|
let responseRef = ref(None)
|
|
362
513
|
let retryRef = ref(0)
|
|
363
|
-
let initialSource = sourceManager.activeSource
|
|
364
|
-
let sourceRef = ref(initialSource)
|
|
365
|
-
let shouldUpdateActiveSource = ref(false)
|
|
366
514
|
|
|
367
515
|
while responseRef.contents->Option.isNone {
|
|
368
|
-
|
|
516
|
+
// Select the best source at the start of every iteration
|
|
517
|
+
let sourceState = switch sourceManager->getNextSource(
|
|
518
|
+
~isLive,
|
|
519
|
+
~excludedSources=?excludedSourcesRef.contents,
|
|
520
|
+
) {
|
|
521
|
+
| Some(s) =>
|
|
522
|
+
if s.source !== sourceManager.activeSource {
|
|
523
|
+
let logger = Logging.createChild(
|
|
524
|
+
~params={"chainId": sourceManager.activeSource.chain->ChainMap.Chain.toChainId},
|
|
525
|
+
)
|
|
526
|
+
logger->Logging.childInfo({
|
|
527
|
+
"msg": "Switching data-source",
|
|
528
|
+
"source": s.source.name,
|
|
529
|
+
"previousSource": sourceManager.activeSource.name,
|
|
530
|
+
"fromBlock": query.fromBlock,
|
|
531
|
+
})
|
|
532
|
+
}
|
|
533
|
+
s
|
|
534
|
+
| None =>
|
|
535
|
+
let logger = Logging.createChild(
|
|
536
|
+
~params={"chainId": sourceManager.activeSource.chain->ChainMap.Chain.toChainId},
|
|
537
|
+
)
|
|
538
|
+
%raw(`null`)->ErrorHandling.mkLogAndRaise(~logger, ~msg=noSourcesError)
|
|
539
|
+
}
|
|
540
|
+
sourceManager.activeSource = sourceState.source
|
|
541
|
+
let source = sourceState.source
|
|
369
542
|
let toBlock = toBlockRef.contents
|
|
370
543
|
let retry = retryRef.contents
|
|
371
544
|
|
|
@@ -389,7 +562,7 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~currentBl
|
|
|
389
562
|
~addressesByContractName=query.addressesByContractName,
|
|
390
563
|
~indexingContracts=query.indexingContracts,
|
|
391
564
|
~partitionId=query.partitionId,
|
|
392
|
-
~
|
|
565
|
+
~knownHeight,
|
|
393
566
|
~selection=query.selection,
|
|
394
567
|
~retry,
|
|
395
568
|
~logger,
|
|
@@ -400,21 +573,20 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~currentBl
|
|
|
400
573
|
"numEvents": response.parsedQueueItems->Array.length,
|
|
401
574
|
"stats": response.stats,
|
|
402
575
|
})
|
|
576
|
+
sourceState.lastFailedAt = None
|
|
403
577
|
responseRef := Some(response)
|
|
404
578
|
} catch {
|
|
405
579
|
| Source.GetItemsError(error) =>
|
|
406
580
|
switch error {
|
|
407
581
|
| UnsupportedSelection(_)
|
|
408
582
|
| FailedGettingFieldSelection(_) => {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
// from sourceManager so it's not attempted anymore
|
|
413
|
-
let notAlreadyDeleted = sourceManager.sources->Utils.Set.delete(source)
|
|
583
|
+
// These errors are impossible to recover, so we disable the source
|
|
584
|
+
// so it's not attempted anymore
|
|
585
|
+
let notAlreadyDisabled = sourceManager->disableSource(sourceState)
|
|
414
586
|
|
|
415
587
|
// In case there are multiple partitions
|
|
416
588
|
// failing at the same time. Log only once
|
|
417
|
-
if
|
|
589
|
+
if notAlreadyDisabled {
|
|
418
590
|
switch error {
|
|
419
591
|
| UnsupportedSelection({message}) => logger->Logging.childError(message)
|
|
420
592
|
| FailedGettingFieldSelection({exn, message, blockNumber, logIndex}) =>
|
|
@@ -428,20 +600,7 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~currentBl
|
|
|
428
600
|
}
|
|
429
601
|
}
|
|
430
602
|
|
|
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
|
-
}
|
|
603
|
+
retryRef := 0
|
|
445
604
|
}
|
|
446
605
|
| FailedGettingItems({attemptedToBlock, retry: WithSuggestedToBlock({toBlock})}) =>
|
|
447
606
|
logger->Logging.childTrace({
|
|
@@ -452,58 +611,24 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~currentBl
|
|
|
452
611
|
toBlockRef := Some(toBlock)
|
|
453
612
|
retryRef := 0
|
|
454
613
|
| FailedGettingItems({exn, attemptedToBlock, retry: ImpossibleForTheQuery({message})}) =>
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
)
|
|
461
|
-
|
|
462
|
-
|
|
614
|
+
// Don't set lastFailedAt — the source isn't broken, the query just can't work on it
|
|
615
|
+
let excludedSources = switch excludedSourcesRef.contents {
|
|
616
|
+
| Some(s) => s
|
|
617
|
+
| None =>
|
|
618
|
+
let s = Utils.Set.make()
|
|
619
|
+
excludedSourcesRef := Some(s)
|
|
620
|
+
s
|
|
621
|
+
}
|
|
622
|
+
excludedSources->Utils.Set.add(sourceState)->ignore
|
|
463
623
|
|
|
464
624
|
logger->Logging.childWarn({
|
|
465
|
-
"msg": message ++
|
|
625
|
+
"msg": message ++ " - Attempting another source",
|
|
466
626
|
"toBlock": attemptedToBlock,
|
|
467
627
|
"err": exn->Utils.prettifyExn,
|
|
468
628
|
})
|
|
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
|
-
}
|
|
629
|
+
retryRef := 0
|
|
480
630
|
|
|
481
631
|
| 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
632
|
// Start displaying warnings after 4 failures
|
|
508
633
|
let log = retry >= 4 ? Logging.childWarn : Logging.childTrace
|
|
509
634
|
logger->log({
|
|
@@ -514,14 +639,30 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~currentBl
|
|
|
514
639
|
"err": exn->Utils.prettifyExn,
|
|
515
640
|
})
|
|
516
641
|
|
|
517
|
-
let shouldSwitch =
|
|
642
|
+
let shouldSwitch = switch retry {
|
|
643
|
+
// Don't attempt a switch on first two failures
|
|
644
|
+
| 0 | 1 => false
|
|
645
|
+
// Then try to switch every second failure
|
|
646
|
+
| _ => retry->mod(2) === 0
|
|
647
|
+
}
|
|
648
|
+
|
|
518
649
|
if shouldSwitch {
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
650
|
+
let now = Js.Date.now()
|
|
651
|
+
sourceState.lastFailedAt = Some(now)
|
|
652
|
+
// Check if there's a working (recovered) source to switch to immediately
|
|
653
|
+
let nextSource =
|
|
654
|
+
sourceManager->getNextSource(~isLive, ~excludedSources=?excludedSourcesRef.contents)
|
|
655
|
+
let hasWorkingAlternative = switch nextSource {
|
|
656
|
+
| Some(s) =>
|
|
657
|
+
switch s.lastFailedAt {
|
|
658
|
+
| None => true
|
|
659
|
+
| Some(failedAt) => now -. failedAt >= sourceManager.recoveryTimeout
|
|
660
|
+
}
|
|
661
|
+
| None => false
|
|
662
|
+
}
|
|
663
|
+
if !hasWorkingAlternative {
|
|
664
|
+
await Utils.delay(Pervasives.min(backoffMillis, 60_000))
|
|
665
|
+
}
|
|
525
666
|
} else {
|
|
526
667
|
await Utils.delay(Pervasives.min(backoffMillis, 60_000))
|
|
527
668
|
}
|
|
@@ -533,9 +674,5 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~currentBl
|
|
|
533
674
|
}
|
|
534
675
|
}
|
|
535
676
|
|
|
536
|
-
if shouldUpdateActiveSource.contents {
|
|
537
|
-
sourceManager.activeSource = sourceRef.contents
|
|
538
|
-
}
|
|
539
|
-
|
|
540
677
|
responseRef.contents->Option.getUnsafe
|
|
541
678
|
}
|