envio 3.13.0-alpha.0 → 3.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -8
- package/src/BatchProcessing.res +4 -5
- package/src/BatchProcessing.res.mjs +4 -11
- package/src/ChainFetching.res +18 -38
- package/src/ChainFetching.res.mjs +14 -20
- package/src/ChainState.res +113 -77
- package/src/ChainState.res.mjs +90 -73
- package/src/ChainState.resi +14 -5
- package/src/Config.res +14 -17
- package/src/Config.res.mjs +5 -20
- package/src/CrossChainState.res +68 -20
- package/src/CrossChainState.res.mjs +48 -5
- package/src/CrossChainState.resi +1 -0
- package/src/Env.res +2 -2
- package/src/ErrorHandling.res +15 -4
- package/src/ErrorHandling.res.mjs +16 -5
- package/src/FinalizeBackfill.res +21 -9
- package/src/FinalizeBackfill.res.mjs +17 -4
- package/src/HandlerRegister.res +0 -37
- package/src/HandlerRegister.res.mjs +0 -18
- package/src/IndexerState.res +2 -0
- package/src/IndexerState.res.mjs +5 -0
- package/src/IndexerState.resi +1 -0
- package/src/Logging.res +6 -38
- package/src/Logging.res.mjs +5 -32
- package/src/Main.res +6 -9
- package/src/Main.res.mjs +3 -5
- package/src/Metrics.res +9 -4
- package/src/Metrics.res.mjs +3 -3
- package/src/Persistence.res +6 -3
- package/src/Persistence.res.mjs +5 -3
- package/src/PgStorage.res +2 -2
- package/src/PgStorage.res.mjs +2 -2
- package/src/Rollback.res +3 -3
- package/src/Server.res +0 -3
- package/src/SimulateItems.res +1 -1
- package/src/SimulateItems.res.mjs +1 -1
- package/src/Supervisor.res +236 -109
- package/src/Supervisor.res.mjs +185 -55
- package/src/TestIndexer.res +6 -3
- package/src/TestIndexer.res.mjs +3 -3
- package/src/Worker.res +6 -0
- package/src/Worker.res.mjs +2 -1
- package/src/bindings/NodeJs.res +25 -16
- package/src/bindings/NodeJs.res.mjs +8 -1
- package/src/sources/AddressSet.res +5 -3
- package/src/sources/AddressStore.res +19 -11
- package/src/sources/AddressStore.res.mjs +2 -7
- package/src/sources/BlockStore.res +10 -9
- package/src/sources/BlockStore.res.mjs +4 -4
- package/src/sources/ChainSources.res +4 -0
- package/src/sources/ChainSources.res.mjs +2 -2
- package/src/sources/EvmChain.res +4 -0
- package/src/sources/EvmChain.res.mjs +3 -1
- package/src/sources/EvmEventItem.res +56 -0
- package/src/sources/EvmEventItem.res.mjs +32 -0
- package/src/sources/EvmHyperSyncSource.res +7 -37
- package/src/sources/EvmHyperSyncSource.res.mjs +8 -29
- package/src/sources/EvmRpcClient.res +41 -59
- package/src/sources/EvmRpcClient.res.mjs +4 -54
- package/src/sources/EvmRpcWs.res +4 -1
- package/src/sources/EvmRpcWs.res.mjs +6 -2
- package/src/sources/FuelHyperSyncSource.res +6 -4
- package/src/sources/FuelHyperSyncSource.res.mjs +7 -9
- package/src/sources/HeightFeed.res +7 -1
- package/src/sources/HeightFeed.res.mjs +9 -7
- package/src/sources/HyperSyncClient.res +1 -14
- package/src/sources/RequestStat.res +7 -0
- package/src/sources/RequestStat.res.mjs +14 -1
- package/src/sources/RpcSource.res +100 -1043
- package/src/sources/RpcSource.res.mjs +81 -1056
- package/src/sources/SimulateSource.res +2 -5
- package/src/sources/SimulateSource.res.mjs +2 -3
- package/src/sources/Source.res +34 -12
- package/src/sources/Source.res.mjs +11 -0
- package/src/sources/SourceManager.res +111 -31
- package/src/sources/SourceManager.res.mjs +65 -31
- package/src/sources/SourceManager.resi +4 -0
- package/src/sources/StartBlockResolver.res +8 -5
- package/src/sources/StartBlockResolver.res.mjs +4 -2
- package/src/sources/SvmHyperSyncSource.res +4 -2
- package/src/sources/SvmHyperSyncSource.res.mjs +4 -6
- package/src/sources/TransactionStore.res +9 -6
- package/src/sources/TransactionStore.res.mjs +2 -2
- package/src/tui/Tui.res +4 -18
- package/src/tui/Tui.res.mjs +3 -6
- package/src/LazyLoader.res +0 -135
- package/src/LazyLoader.res.mjs +0 -118
- package/src/bindings/SDSL.res +0 -12
- package/src/bindings/SDSL.res.mjs +0 -9
- package/src/bindings/Yargs.res +0 -8
- package/src/bindings/Yargs.res.mjs +0 -2
- package/src/sources/Rpc.res +0 -185
- package/src/sources/Rpc.res.mjs +0 -183
package/src/Supervisor.res
CHANGED
|
@@ -6,17 +6,26 @@ type worker = {chainIds: array<ChainId.t>, maxConnections: int}
|
|
|
6
6
|
// on a single one, so the budget buys workers two at a time.
|
|
7
7
|
let minConnectionsPerWorker = 2
|
|
8
8
|
|
|
9
|
+
// Most processes a run is split into, however much budget it is given. A worker
|
|
10
|
+
// is a whole Node process with its own heap, handler modules and source
|
|
11
|
+
// clients, and however many of them a run has, they share one machine. Past
|
|
12
|
+
// this a raised budget widens the workers' pools rather than adding workers.
|
|
13
|
+
let maxWorkers = 4
|
|
14
|
+
|
|
9
15
|
// How to spend a connection budget on the chains a run indexes. `None` keeps
|
|
10
|
-
// the run in one process
|
|
11
|
-
// workers, or a config with nothing to split, has to do.
|
|
16
|
+
// the run in one process.
|
|
12
17
|
//
|
|
13
|
-
// Chains are dealt in config order and the direction reverses each pass, so
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
+
// Chains are dealt in config order and the direction reverses each pass, so the
|
|
19
|
+
// first chains lead different workers and the worker that took the first picks
|
|
20
|
+
// up the last. How much work a chain has is the contracts' to decide, so config
|
|
21
|
+
// order is the only ranking the run can be given: listing chains busiest-first
|
|
22
|
+
// in config.yaml is what balances the layout.
|
|
18
23
|
let plan = (~chainIds: array<ChainId.t>, ~maxConnections: int): option<array<worker>> => {
|
|
19
|
-
let workerCount =
|
|
24
|
+
let workerCount =
|
|
25
|
+
[chainIds->Array.length, maxConnections / minConnectionsPerWorker, maxWorkers]->Array.reduce(
|
|
26
|
+
maxWorkers,
|
|
27
|
+
Pervasives.min,
|
|
28
|
+
)
|
|
20
29
|
if workerCount < 2 {
|
|
21
30
|
None
|
|
22
31
|
} else {
|
|
@@ -49,11 +58,10 @@ let planForRun = (~config: Config.t, ~maxConnections=Env.Db.maxConnections) =>
|
|
|
49
58
|
}
|
|
50
59
|
|
|
51
60
|
// One forked worker: the process, the chains it drives, and the last snapshot
|
|
52
|
-
// it reported.
|
|
53
|
-
// heard from anyone yet render as initializing rather than as empty.
|
|
61
|
+
// it reported.
|
|
54
62
|
type running = {
|
|
55
63
|
worker: worker,
|
|
56
|
-
child: NodeJs.ChildProcess.
|
|
64
|
+
child: NodeJs.ChildProcess.Child.t,
|
|
57
65
|
mutable snapshot: option<Metrics.t>,
|
|
58
66
|
mutable runtime: option<Metrics.runtimeSample>,
|
|
59
67
|
// A spawn failure can raise `error` and `exit` both, and a worker counted
|
|
@@ -61,8 +69,6 @@ type running = {
|
|
|
61
69
|
mutable settled: bool,
|
|
62
70
|
}
|
|
63
71
|
|
|
64
|
-
// A worker is named by the chains it drives, which is what an operator reading
|
|
65
|
-
// its memory or its event loop wants to know.
|
|
66
72
|
let name = (worker: worker) => worker.chainIds->Array.map(ChainId.toString)->Array.joinUnsafe(";")
|
|
67
73
|
|
|
68
74
|
let label = (worker: worker) => `[chain ${worker->name}]`
|
|
@@ -106,18 +112,36 @@ let readLines = (~onLine) => {
|
|
|
106
112
|
(read, flush)
|
|
107
113
|
}
|
|
108
114
|
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
115
|
+
// Each worker's share of the run's memory budgets. Both are the whole indexer's
|
|
116
|
+
// rather than one process's, so workers that each took the whole of one would
|
|
117
|
+
// hold as many times the memory as the run happened to have workers.
|
|
118
|
+
let memoryBudgets = (~workerCount) =>
|
|
119
|
+
[
|
|
120
|
+
("ENVIO_INDEXING_MAX_BUFFER_SIZE", CrossChainState.calculateTargetBufferSize()),
|
|
121
|
+
("ENVIO_IN_MEMORY_OBJECTS_TARGET", Env.inMemoryObjectsTarget->Float.toInt),
|
|
122
|
+
]->Array.map(((name, budget)) => (
|
|
123
|
+
name,
|
|
124
|
+
// A budget smaller than the run has workers still leaves each one something
|
|
125
|
+
// to hold, rather than a pool it can never put anything in.
|
|
126
|
+
Pervasives.max(1, budget / workerCount)->Int.toString,
|
|
127
|
+
))
|
|
128
|
+
|
|
129
|
+
// `pino-pretty` colorizes on this test, and a piped worker would fail it for a
|
|
130
|
+
// run the operator is watching in colour.
|
|
112
131
|
@val external stdoutIsTty: Nullable.t<bool> = "process.stdout.isTTY"
|
|
113
132
|
|
|
114
133
|
let fork = (
|
|
115
134
|
worker: worker,
|
|
116
135
|
~workerIndex,
|
|
136
|
+
// How many processes the run's budgets are being split between.
|
|
137
|
+
~workerCount,
|
|
117
138
|
// Whether this worker waits for the run before going realtime. False when
|
|
118
139
|
// every chain resumed already caught up: there is nothing left to wait for,
|
|
119
140
|
// and a barrier nobody can open would hold the run forever.
|
|
120
141
|
~holdRealtime,
|
|
142
|
+
// Which command the run was started by, which a worker's own parse of the
|
|
143
|
+
// project's files can't tell it.
|
|
144
|
+
~isDev,
|
|
121
145
|
// The entry this process was itself started from, so a worker is the same
|
|
122
146
|
// program as its supervisor however the package was installed.
|
|
123
147
|
~entryPath=NodeJs.Process.argv->Array.getUnsafe(1),
|
|
@@ -125,17 +149,22 @@ let fork = (
|
|
|
125
149
|
// them write to the terminal behind the frame's back.
|
|
126
150
|
~pipeOutput=false,
|
|
127
151
|
~onOutput=Console.log,
|
|
152
|
+
~onErrorOutput=Console.error,
|
|
153
|
+
~onSnapshot=() => (),
|
|
128
154
|
) => {
|
|
129
155
|
let env = NodeJs.Process.process.env->Dict.copy
|
|
130
156
|
env->Dict.set(
|
|
131
157
|
Worker.envVar,
|
|
132
|
-
{
|
|
133
|
-
Worker.
|
|
134
|
-
|
|
158
|
+
{
|
|
159
|
+
Worker.chainIds: worker.chainIds,
|
|
160
|
+
holdRealtime,
|
|
161
|
+
isDev,
|
|
162
|
+
}->S.reverseConvertToJsonStringOrThrow(Worker.configSchema),
|
|
135
163
|
)
|
|
136
|
-
// The worker's slice of the
|
|
137
|
-
// loads, which is why
|
|
164
|
+
// The worker's slice of the budgets. Read when the worker's own Env module
|
|
165
|
+
// loads, which is why they ride in the spawn environment rather than a message.
|
|
138
166
|
env->Dict.set("ENVIO_PG_MAX_CONNECTIONS", worker.maxConnections->Int.toString)
|
|
167
|
+
memoryBudgets(~workerCount)->Array.forEach(((name, share)) => env->Dict.set(name, share))
|
|
139
168
|
env->Dict.set("LOG_FILE", logFilePath(~workerIndex))
|
|
140
169
|
if pipeOutput && stdoutIsTty->Nullable.toOption->Option.getOr(false) {
|
|
141
170
|
env->Dict.set("FORCE_COLOR", "1")
|
|
@@ -153,26 +182,32 @@ let fork = (
|
|
|
153
182
|
},
|
|
154
183
|
)
|
|
155
184
|
if pipeOutput {
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
|
|
185
|
+
// The supervisor writes a worker's lines the way it writes its own, which is
|
|
186
|
+
// the only way ink can keep them out of its frame. Each stream keeps the one
|
|
187
|
+
// it was written to, so a worker's errors stay on stderr for whoever is
|
|
188
|
+
// redirecting it.
|
|
189
|
+
[
|
|
190
|
+
(child->NodeJs.ChildProcess.Child.stdout, onOutput),
|
|
191
|
+
(child->NodeJs.ChildProcess.Child.stderr, onErrorOutput),
|
|
192
|
+
]->Array.forEach(((stream, onLine)) =>
|
|
159
193
|
switch stream->Null.toOption {
|
|
160
194
|
| Some(stream) => {
|
|
161
|
-
let (read, flush) = readLines(~onLine
|
|
162
|
-
stream->NodeJs.ChildProcess.setEncoding("utf8")
|
|
163
|
-
stream->NodeJs.ChildProcess.onData(read)
|
|
164
|
-
stream->NodeJs.ChildProcess.onEnd(flush)
|
|
195
|
+
let (read, flush) = readLines(~onLine)
|
|
196
|
+
stream->NodeJs.ChildProcess.Stream.setEncoding("utf8")
|
|
197
|
+
stream->NodeJs.ChildProcess.Stream.onData(read)
|
|
198
|
+
stream->NodeJs.ChildProcess.Stream.onEnd(flush)
|
|
165
199
|
}
|
|
166
200
|
| None => ()
|
|
167
201
|
}
|
|
168
202
|
)
|
|
169
203
|
}
|
|
170
204
|
let running = {worker, child, snapshot: None, runtime: None, settled: false}
|
|
171
|
-
child->NodeJs.ChildProcess.onMessage(message =>
|
|
205
|
+
child->NodeJs.ChildProcess.Child.onMessage(message =>
|
|
172
206
|
switch message {
|
|
173
207
|
| Worker.Snapshot({metrics, runtime}) => {
|
|
174
208
|
running.snapshot = Some(metrics)
|
|
175
209
|
running.runtime = Some(runtime)
|
|
210
|
+
onSnapshot()
|
|
176
211
|
}
|
|
177
212
|
}
|
|
178
213
|
)
|
|
@@ -180,23 +215,25 @@ let fork = (
|
|
|
180
215
|
}
|
|
181
216
|
|
|
182
217
|
// The forked workers of one run, and whether their supervisor is the one
|
|
183
|
-
// taking them down
|
|
184
|
-
|
|
185
|
-
|
|
218
|
+
// taking them down, which is what tells an expected exit from the rest.
|
|
219
|
+
type group = {
|
|
220
|
+
// Assigned once the forks are made, which is after the group exists: a
|
|
221
|
+
// worker's report asks the group whether the run may go realtime.
|
|
222
|
+
mutable running: array<running>,
|
|
223
|
+
mutable stopping: bool,
|
|
224
|
+
// Whether the workers are still waiting for the run's leave to go realtime.
|
|
225
|
+
mutable holdingRealtime: bool,
|
|
226
|
+
}
|
|
186
227
|
|
|
187
228
|
let stop = group => {
|
|
188
229
|
group.stopping = true
|
|
189
|
-
group.running->Array.forEach(r => r.child->NodeJs.ChildProcess.kill("SIGTERM")->ignore)
|
|
230
|
+
group.running->Array.forEach(r => r.child->NodeJs.ChildProcess.Child.kill("SIGTERM")->ignore)
|
|
190
231
|
}
|
|
191
232
|
|
|
192
|
-
// The dev console's cache dump
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
// time. Nothing in it is a worker's to know — the rows it copies are the ones
|
|
197
|
-
// already committed.
|
|
198
|
-
//
|
|
199
|
-
// Requests that overlap join the dump in flight, for the same reason.
|
|
233
|
+
// The dev console's cache dump belongs to the supervisor: a dump copies every
|
|
234
|
+
// effect cache table in the schema, so a worker asked to do it would copy its
|
|
235
|
+
// siblings' chains too, and several asked at once would write the same files at
|
|
236
|
+
// the same time. Overlapping requests join the dump in flight for that reason.
|
|
200
237
|
let syncCache = {
|
|
201
238
|
let inFlight = ref(None)
|
|
202
239
|
(~dump) =>
|
|
@@ -210,10 +247,9 @@ let syncCache = {
|
|
|
210
247
|
}
|
|
211
248
|
|
|
212
249
|
// The supervisor handed its connections to the workers, so a dump opens one of
|
|
213
|
-
// its own
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
// the pool is capped at that one.
|
|
250
|
+
// its own and puts the run one connection over its budget — deliberately: only
|
|
251
|
+
// `envio dev` asks for a dump, and the alternative is pausing the indexing to
|
|
252
|
+
// free one.
|
|
217
253
|
let dumpCache = (~config) => {
|
|
218
254
|
let storage = PgStorage.makeStorageFromEnv(~config, ~sql=PgStorage.makeClient(~maxConnections=1))
|
|
219
255
|
storage.dumpEffectCache()->Promise.finally(() => storage.close()->Promise.ignore)
|
|
@@ -223,6 +259,37 @@ let dumpCache = (~config) => {
|
|
|
223
259
|
// which is what indexing to every end block looks like.
|
|
224
260
|
type outcome = Finished | Stopped
|
|
225
261
|
|
|
262
|
+
// How one worker's ending reads.
|
|
263
|
+
type ending =
|
|
264
|
+
// On its own terms, or because the supervisor asked.
|
|
265
|
+
| Expected
|
|
266
|
+
// Asked to stop by someone other than the supervisor. A process manager that
|
|
267
|
+
// signals a whole group reaches the workers itself, so a worker can be told
|
|
268
|
+
// before the supervisor has decided what the signal meant.
|
|
269
|
+
| Stopping
|
|
270
|
+
| Failed
|
|
271
|
+
|
|
272
|
+
// A worker that stops on a signal is being stopped, not failing: `systemctl
|
|
273
|
+
// stop` on a unit with the default `KillMode=control-group` sends SIGTERM to
|
|
274
|
+
// every process in it, so the workers get it directly and exit on it. Reading
|
|
275
|
+
// that as a failure would fail every clean shutdown under systemd.
|
|
276
|
+
//
|
|
277
|
+
// The kernel's out-of-memory killer sends SIGKILL, which stays a failure — as
|
|
278
|
+
// does every non-zero exit of a worker the supervisor didn't ask to stop.
|
|
279
|
+
//
|
|
280
|
+
// Once the supervisor is stopping, though, every exit reads as expected and the
|
|
281
|
+
// run exits 0: a worker that crashes on its way down is indistinguishable from
|
|
282
|
+
// one that took the SIGTERM, and a stop that reported a failure would fail
|
|
283
|
+
// every restart the crash happened to race.
|
|
284
|
+
let classifyExit = (~code: Null.t<int>, ~signal: Null.t<string>, ~stopping) =>
|
|
285
|
+
switch (stopping, code->Null.toOption, signal->Null.toOption) {
|
|
286
|
+
| (true, _, _)
|
|
287
|
+
| (_, Some(0), _) =>
|
|
288
|
+
Expected
|
|
289
|
+
| (_, _, Some("SIGTERM")) => Stopping
|
|
290
|
+
| _ => Failed
|
|
291
|
+
}
|
|
292
|
+
|
|
226
293
|
// Resolves once every worker has ended. Throws if any of them ended in a way
|
|
227
294
|
// the supervisor didn't ask for, having first taken the rest down: one worker
|
|
228
295
|
// short leaves its chains unindexed, and a run that kept the others going would
|
|
@@ -232,12 +299,16 @@ let awaitExit = async (group): outcome => {
|
|
|
232
299
|
let alive = ref(group.running->Array.length)
|
|
233
300
|
|
|
234
301
|
await Promise.make((resolve, _) => {
|
|
235
|
-
let onGone = (r, ~
|
|
302
|
+
let onGone = (r, ~ending) =>
|
|
236
303
|
if !r.settled {
|
|
237
304
|
r.settled = true
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
305
|
+
// The rest of the run goes down either way; what differs is whether the
|
|
306
|
+
// run reports itself as having failed.
|
|
307
|
+
switch ending {
|
|
308
|
+
| Expected => ()
|
|
309
|
+
| Stopping => group->stop
|
|
310
|
+
| Failed => {
|
|
311
|
+
failed := true
|
|
241
312
|
group->stop
|
|
242
313
|
}
|
|
243
314
|
}
|
|
@@ -248,16 +319,13 @@ let awaitExit = async (group): outcome => {
|
|
|
248
319
|
}
|
|
249
320
|
|
|
250
321
|
group.running->Array.forEach(r => {
|
|
251
|
-
r.child->NodeJs.ChildProcess.onExit(
|
|
252
|
-
(code,
|
|
253
|
-
// Only an exit the supervisor asked for is expected. Anything else — a
|
|
254
|
-
// non-zero code, or a signal like the kernel's out-of-memory kill.
|
|
255
|
-
r->onGone(~failure=!group.stopping && code->Null.toOption !== Some(0)),
|
|
322
|
+
r.child->NodeJs.ChildProcess.Child.onExit(
|
|
323
|
+
(code, signal) => r->onGone(~ending=classifyExit(~code, ~signal, ~stopping=group.stopping)),
|
|
256
324
|
)
|
|
257
|
-
r.child->NodeJs.ChildProcess.
|
|
325
|
+
r.child->NodeJs.ChildProcess.Child.onError(
|
|
258
326
|
exn => {
|
|
259
327
|
Logging.errorWithExn(exn, `${r.worker->label} failed to start`)
|
|
260
|
-
r->onGone(~
|
|
328
|
+
r->onGone(~ending=Failed)
|
|
261
329
|
},
|
|
262
330
|
)
|
|
263
331
|
})
|
|
@@ -269,18 +337,88 @@ let awaitExit = async (group): outcome => {
|
|
|
269
337
|
group.stopping ? Stopped : Finished
|
|
270
338
|
}
|
|
271
339
|
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
//
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
let isRunAtHead = (
|
|
281
|
-
|
|
282
|
-
|
|
340
|
+
// Whether a run holding its workers back may let them go: every worker is still
|
|
341
|
+
// there to be released, has reported, and has got as far as it can on its own.
|
|
342
|
+
//
|
|
343
|
+
// A worker that is gone leaves the run a process short, so there is nothing to
|
|
344
|
+
// release it into, and its last snapshot outlives it. The channel is what says
|
|
345
|
+
// so: Node closes it before it reports the exit, so a process on its way out
|
|
346
|
+
// still reads as running everywhere else, and the release sent to it comes back
|
|
347
|
+
// as the error a supervisor reports as a worker failing to start.
|
|
348
|
+
let isRunAtHead = (running: array<running>) =>
|
|
349
|
+
running->Utils.Array.notEmpty &&
|
|
350
|
+
running->Array.every(r =>
|
|
351
|
+
r.child->NodeJs.ChildProcess.Child.connected &&
|
|
352
|
+
r.snapshot->Option.mapOr(false, snapshot => snapshot.hasArrivedAtHead)
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
// Holds every worker at the head until the last of them arrives, then releases
|
|
356
|
+
// them together. Chains enter the reorg threshold and go realtime as one
|
|
357
|
+
// indexer, and in a split run only the supervisor can see when that is.
|
|
358
|
+
//
|
|
359
|
+
// Asked on every report rather than on a clock of its own: a report is the only
|
|
360
|
+
// thing that can change the answer.
|
|
361
|
+
let releaseIfAtHead = group =>
|
|
362
|
+
if group.holdingRealtime && !group.stopping && group.running->isRunAtHead {
|
|
363
|
+
group.holdingRealtime = false
|
|
364
|
+
group.running->Array.forEach(r =>
|
|
365
|
+
r.child->NodeJs.ChildProcess.Child.send(Worker.ReleaseRealtime)->ignore
|
|
283
366
|
)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// The run's chains as an unsplit indexer reports them before it has fetched
|
|
370
|
+
// anything: at their configured blocks, with nothing indexed. A display that
|
|
371
|
+
// hasn't heard from a worker yet draws these, rather than the indexer with no
|
|
372
|
+
// chains at all that an empty merge would render.
|
|
373
|
+
let configuredChains = (config: Config.t): array<Metrics.chainMetrics> =>
|
|
374
|
+
config.chainMap
|
|
375
|
+
->ChainMap.values
|
|
376
|
+
->Array.map((chain): Metrics.chainMetrics => {
|
|
377
|
+
chainId: chain.id,
|
|
378
|
+
poweredByHyperSync: switch chain.sourceConfig {
|
|
379
|
+
| EvmSourceConfig({hypersync}) => hypersync->Option.isSome
|
|
380
|
+
| FuelSourceConfig(_) | SvmSourceConfig(_) => true
|
|
381
|
+
| SimulateSourceConfig(_) | CustomSources(_) => false
|
|
382
|
+
},
|
|
383
|
+
firstEventBlockNumber: None,
|
|
384
|
+
latestProcessedBlock: None,
|
|
385
|
+
timestampCaughtUpToHeadOrEndblock: None,
|
|
386
|
+
numEventsProcessed: 0.,
|
|
387
|
+
latestFetchedBlockNumber: 0,
|
|
388
|
+
knownHeight: 0,
|
|
389
|
+
numBatchesFetched: 0,
|
|
390
|
+
// A chain resolves `start_block: latest` against its own head as it starts,
|
|
391
|
+
// which is a worker's to do and no supervisor's to guess.
|
|
392
|
+
startBlock: switch chain.startBlock {
|
|
393
|
+
| Block(block) => block
|
|
394
|
+
| Latest => 0
|
|
395
|
+
},
|
|
396
|
+
endBlock: chain.endBlock,
|
|
397
|
+
numAddresses: 0,
|
|
398
|
+
addressesByContract: [],
|
|
399
|
+
isReady: false,
|
|
400
|
+
sourceBlockNumber: 0,
|
|
401
|
+
progressBlockNumber: -1,
|
|
402
|
+
progressLatencyMs: None,
|
|
403
|
+
progressBlockTime: None,
|
|
404
|
+
concurrency: 0,
|
|
405
|
+
partitionsCount: 0,
|
|
406
|
+
bufferSize: 0,
|
|
407
|
+
bufferBlockNumber: -1,
|
|
408
|
+
idleSeconds: 0.,
|
|
409
|
+
waitingForNewBlockSeconds: 0.,
|
|
410
|
+
queryingSeconds: 0.,
|
|
411
|
+
blockRangeFetchSeconds: 0.,
|
|
412
|
+
blockRangeParseSeconds: 0.,
|
|
413
|
+
blockRangeFetchCount: 0.,
|
|
414
|
+
blockRangeFetchedEvents: 0.,
|
|
415
|
+
blockRangeFetchedBlocks: 0.,
|
|
416
|
+
reorgCount: 0,
|
|
417
|
+
reorgDetectedBlock: None,
|
|
418
|
+
rollbackTargetBlock: None,
|
|
419
|
+
rateLimitTimeMs: 0.,
|
|
420
|
+
rateLimitResetInMs: None,
|
|
421
|
+
})
|
|
284
422
|
|
|
285
423
|
// Runs the group: creates the schema for every chain, forks a worker per plan
|
|
286
424
|
// entry, and serves the run's metrics, console and display from what they
|
|
@@ -303,14 +441,15 @@ let run = async (~config: Config.t, ~workers: array<worker>, ~reset) => {
|
|
|
303
441
|
let startTime = Date.make()
|
|
304
442
|
let startTimeRef = Performance.now()
|
|
305
443
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
444
|
+
// The counts ride as fields rather than in the sentence: the connection limit
|
|
445
|
+
// is the only setting that decides any of this, and a reader who wants to
|
|
446
|
+
// change it has nothing else to go on.
|
|
447
|
+
Logging.info({
|
|
448
|
+
"msg": "Indexing will be split across multiple processes for faster and more reliable indexing.",
|
|
449
|
+
"chains": config.chainMap->ChainMap.values->Array.length,
|
|
450
|
+
"processes": workers->Array.length,
|
|
451
|
+
"maxConnections": Env.Db.maxConnections,
|
|
452
|
+
})
|
|
314
453
|
|
|
315
454
|
// Decided before the first fork: it is what makes a worker's output the
|
|
316
455
|
// supervisor's to print.
|
|
@@ -321,12 +460,18 @@ let run = async (~config: Config.t, ~workers: array<worker>, ~reset) => {
|
|
|
321
460
|
(persistence->Persistence.getInitializedState).chains->Array.some(chain =>
|
|
322
461
|
chain.timestampCaughtUpToHeadOrEndblock->Option.isNone
|
|
323
462
|
)
|
|
324
|
-
let group = {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
463
|
+
let group = {running: [], stopping: false, holdingRealtime: holdRealtime}
|
|
464
|
+
group.running =
|
|
465
|
+
workers->Array.mapWithIndex((worker, workerIndex) =>
|
|
466
|
+
worker->fork(
|
|
467
|
+
~workerIndex,
|
|
468
|
+
~workerCount=workers->Array.length,
|
|
469
|
+
~holdRealtime,
|
|
470
|
+
~isDev=config.isDev,
|
|
471
|
+
~pipeOutput=shouldUseTui,
|
|
472
|
+
~onSnapshot=() => group->releaseIfAtHead,
|
|
473
|
+
)
|
|
474
|
+
)
|
|
330
475
|
|
|
331
476
|
let reported = () => group.running->Array.filterMap(r => r.snapshot)
|
|
332
477
|
let merge = snapshots =>
|
|
@@ -335,6 +480,10 @@ let run = async (~config: Config.t, ~workers: array<worker>, ~reset) => {
|
|
|
335
480
|
~startTime,
|
|
336
481
|
~metricTime=Date.make(),
|
|
337
482
|
~elapsedSeconds=startTimeRef->Performance.secondsSince,
|
|
483
|
+
// The run's pool, which its workers hold a share of each. Reporting the
|
|
484
|
+
// shares added back up would say the same thing less directly, and say
|
|
485
|
+
// nothing at all before every worker has reported.
|
|
486
|
+
~targetBufferSize=CrossChainState.calculateTargetBufferSize(),
|
|
338
487
|
)
|
|
339
488
|
|
|
340
489
|
Server.startServer(
|
|
@@ -358,32 +507,13 @@ let run = async (~config: Config.t, ~workers: array<worker>, ~reset) => {
|
|
|
358
507
|
~onSyncCache=() => syncCache(~dump=() => dumpCache(~config)),
|
|
359
508
|
)
|
|
360
509
|
|
|
361
|
-
// Chains enter the reorg threshold and go realtime as one indexer, which in a
|
|
362
|
-
// split run only the supervisor can see. Every worker is held until the last
|
|
363
|
-
// one arrives, then released together, so the run switches over exactly as an
|
|
364
|
-
// unsplit one does.
|
|
365
|
-
let releaseCheck = ref(None)
|
|
366
|
-
let stopReleaseCheck = () => {
|
|
367
|
-
releaseCheck.contents->Option.forEach(clearInterval)
|
|
368
|
-
releaseCheck := None
|
|
369
|
-
}
|
|
370
|
-
if holdRealtime {
|
|
371
|
-
releaseCheck :=
|
|
372
|
-
Some(
|
|
373
|
-
setInterval(() =>
|
|
374
|
-
if reported()->isRunAtHead(~workerCount=group.running->Array.length) {
|
|
375
|
-
stopReleaseCheck()
|
|
376
|
-
group.running->Array.forEach(r =>
|
|
377
|
-
r.child->NodeJs.ChildProcess.send(Worker.ReleaseRealtime)->ignore
|
|
378
|
-
)
|
|
379
|
-
Logging.info("Every chain has reached the head. Switching the run to realtime.")
|
|
380
|
-
}
|
|
381
|
-
, releaseCheckIntervalMillis),
|
|
382
|
-
)
|
|
383
|
-
}
|
|
384
|
-
|
|
385
510
|
if shouldUseTui {
|
|
386
|
-
let _rerender = Tui.start(~config, ~getMetrics=() =>
|
|
511
|
+
let _rerender = Tui.start(~config, ~getMetrics=() =>
|
|
512
|
+
switch reported() {
|
|
513
|
+
| [] => {...[]->merge, chains: configuredChains(config)}
|
|
514
|
+
| snapshots => snapshots->merge
|
|
515
|
+
}
|
|
516
|
+
)
|
|
387
517
|
}
|
|
388
518
|
|
|
389
519
|
// Whichever signal asks the run to stop, the supervisor is the one that
|
|
@@ -397,9 +527,6 @@ let run = async (~config: Config.t, ~workers: array<worker>, ~reset) => {
|
|
|
397
527
|
// is the exception, as it is for a single process: it keeps the final state
|
|
398
528
|
// on screen until the terminal closes it.
|
|
399
529
|
let outcome = await group->awaitExit
|
|
400
|
-
// Nothing left to release, and a display keeps this process alive long enough
|
|
401
|
-
// for the check to reach children that are gone.
|
|
402
|
-
stopReleaseCheck()
|
|
403
530
|
|
|
404
531
|
switch outcome {
|
|
405
532
|
| Stopped => NodeJs.process->NodeJs.exitWithCode(Success)
|