envio 3.12.1 → 3.13.0-rc.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 +15 -14
- package/src/BatchProcessing.res.mjs +8 -15
- package/src/Bin.res +24 -17
- package/src/Bin.res.mjs +5 -0
- package/src/ChainFetching.res +18 -24
- package/src/ChainFetching.res.mjs +14 -15
- package/src/ChainState.res +103 -38
- package/src/ChainState.res.mjs +79 -22
- package/src/ChainState.resi +14 -2
- package/src/Config.res +32 -0
- package/src/Config.res.mjs +24 -0
- package/src/Core.res +4 -0
- package/src/Core.res.mjs +4 -0
- package/src/CrossChainState.res +109 -11
- package/src/CrossChainState.res.mjs +71 -7
- package/src/CrossChainState.resi +11 -1
- package/src/Env.res +4 -0
- package/src/ErrorHandling.res +15 -4
- package/src/ErrorHandling.res.mjs +16 -5
- package/src/FinalizeBackfill.res +10 -4
- package/src/FinalizeBackfill.res.mjs +5 -3
- package/src/HandlerRegister.res +0 -37
- package/src/HandlerRegister.res.mjs +0 -18
- package/src/IndexerLoop.res +2 -0
- package/src/IndexerLoop.res.mjs +1 -0
- package/src/IndexerState.res +43 -1
- package/src/IndexerState.res.mjs +48 -4
- package/src/IndexerState.resi +11 -0
- package/src/Main.res +128 -268
- package/src/Main.res.mjs +27 -153
- package/src/Metrics.res +268 -102
- package/src/Metrics.res.mjs +227 -48
- package/src/Persistence.res +30 -2
- package/src/Persistence.res.mjs +12 -3
- package/src/PgStorage.res +19 -11
- package/src/PgStorage.res.mjs +13 -10
- package/src/Rollback.res +3 -3
- package/src/Server.res +178 -0
- package/src/Server.res.mjs +143 -0
- package/src/SimulateItems.res +1 -1
- package/src/SimulateItems.res.mjs +1 -1
- package/src/Supervisor.res +542 -0
- package/src/Supervisor.res.mjs +455 -0
- package/src/TestIndexer.res +1 -1
- package/src/TestIndexer.res.mjs +2 -2
- package/src/Worker.res +101 -0
- package/src/Worker.res.mjs +81 -0
- package/src/bindings/NodeJs.res +50 -0
- package/src/bindings/NodeJs.res.mjs +8 -1
- package/src/db/InternalTable.res +8 -1
- package/src/db/InternalTable.res.mjs +5 -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 +10 -0
- package/src/tui/Tui.res.mjs +15 -0
- package/src/tui/components/SyncETA.res +12 -6
- package/src/tui/components/SyncETA.res.mjs +12 -8
- 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
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
// One worker process and the chains it drives. `maxConnections` is its slice of
|
|
2
|
+
// the run's connection budget, which the pool it opens is capped to.
|
|
3
|
+
type worker = {chainIds: array<ChainId.t>, maxConnections: int}
|
|
4
|
+
|
|
5
|
+
// Every worker needs enough connections to read and write without serializing
|
|
6
|
+
// on a single one, so the budget buys workers two at a time.
|
|
7
|
+
let minConnectionsPerWorker = 2
|
|
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
|
+
|
|
15
|
+
// How to spend a connection budget on the chains a run indexes. `None` keeps
|
|
16
|
+
// the run in one process.
|
|
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.
|
|
23
|
+
let plan = (~chainIds: array<ChainId.t>, ~maxConnections: int): option<array<worker>> => {
|
|
24
|
+
let workerCount =
|
|
25
|
+
[chainIds->Array.length, maxConnections / minConnectionsPerWorker, maxWorkers]->Array.reduce(
|
|
26
|
+
maxWorkers,
|
|
27
|
+
Pervasives.min,
|
|
28
|
+
)
|
|
29
|
+
if workerCount < 2 {
|
|
30
|
+
None
|
|
31
|
+
} else {
|
|
32
|
+
// The remainder is handed out one connection at a time rather than left
|
|
33
|
+
// unspent, so a budget with slack widens the earliest workers' pools.
|
|
34
|
+
let evenShare = maxConnections / workerCount
|
|
35
|
+
let remainder = mod(maxConnections, workerCount)
|
|
36
|
+
Some(
|
|
37
|
+
Array.fromInitializer(~length=workerCount, workerIndex => {
|
|
38
|
+
chainIds: chainIds->Array.filterWithIndex((_, dealIndex) => {
|
|
39
|
+
let position = mod(dealIndex, workerCount)
|
|
40
|
+
let isReversePass = mod(dealIndex / workerCount, 2) === 1
|
|
41
|
+
(isReversePass ? workerCount - 1 - position : position) === workerIndex
|
|
42
|
+
}),
|
|
43
|
+
maxConnections: evenShare + (workerIndex < remainder ? 1 : 0),
|
|
44
|
+
}),
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Whether this run splits, and how. A schema that shares entities across chains
|
|
50
|
+
// can't be split: workers each advance their own checkpoint sequence, which only
|
|
51
|
+
// holds while no entity has rows another chain can reach. A run that is already
|
|
52
|
+
// one chain's process doesn't split again — whoever started it owns the layout.
|
|
53
|
+
let planForRun = (~config: Config.t, ~maxConnections=Env.Db.maxConnections) =>
|
|
54
|
+
if config.isolated || !(config->Config.isPerChain) {
|
|
55
|
+
None
|
|
56
|
+
} else {
|
|
57
|
+
plan(~chainIds=config.chainMap->ChainMap.values->Array.map(chain => chain.id), ~maxConnections)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// One forked worker: the process, the chains it drives, and the last snapshot
|
|
61
|
+
// it reported.
|
|
62
|
+
type running = {
|
|
63
|
+
worker: worker,
|
|
64
|
+
child: NodeJs.ChildProcess.Child.t,
|
|
65
|
+
mutable snapshot: option<Metrics.t>,
|
|
66
|
+
mutable runtime: option<Metrics.runtimeSample>,
|
|
67
|
+
// A spawn failure can raise `error` and `exit` both, and a worker counted
|
|
68
|
+
// twice would end the run while its siblings are still indexing.
|
|
69
|
+
mutable settled: bool,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let name = (worker: worker) => worker.chainIds->Array.map(ChainId.toString)->Array.joinUnsafe(";")
|
|
73
|
+
|
|
74
|
+
let label = (worker: worker) => `[chain ${worker->name}]`
|
|
75
|
+
|
|
76
|
+
// Workers append to files of their own. Pino writes a line per call, and
|
|
77
|
+
// several processes appending to one file can still tear a long line apart.
|
|
78
|
+
let logFilePath = (~workerIndex, ~path=Env.logFilePath) => {
|
|
79
|
+
let suffix = `.worker-${workerIndex->Int.toString}`
|
|
80
|
+
// A dot in a directory name isn't an extension, and a path with no dot at
|
|
81
|
+
// all has none either: `./logs/envio` takes the suffix at the end.
|
|
82
|
+
let dot = path->String.lastIndexOf(".")
|
|
83
|
+
if dot > path->String.lastIndexOf("/") {
|
|
84
|
+
`${path->String.slice(~start=0, ~end=dot)}${suffix}${path->String.slice(
|
|
85
|
+
~start=dot,
|
|
86
|
+
~end=path->String.length,
|
|
87
|
+
)}`
|
|
88
|
+
} else {
|
|
89
|
+
`${path}${suffix}`
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// A pipe hands over whatever has been flushed, so a chunk boundary falls
|
|
94
|
+
// wherever the OS put it: the tail of a chunk is a line only once the chunk
|
|
95
|
+
// that ends it arrives. Reading pairs with a flush, since a process that dies
|
|
96
|
+
// mid-line still wrote what it managed to — which is when it matters most.
|
|
97
|
+
let readLines = (~onLine) => {
|
|
98
|
+
let pending = ref("")
|
|
99
|
+
let read = chunk => {
|
|
100
|
+
let parts = (pending.contents ++ chunk)->String.split("\n")
|
|
101
|
+
pending := parts->Array.pop->Option.getOr("")
|
|
102
|
+
parts->Array.forEach(onLine)
|
|
103
|
+
}
|
|
104
|
+
let flush = () =>
|
|
105
|
+
switch pending.contents {
|
|
106
|
+
| "" => ()
|
|
107
|
+
| line => {
|
|
108
|
+
pending := ""
|
|
109
|
+
onLine(line)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
(read, flush)
|
|
113
|
+
}
|
|
114
|
+
|
|
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.
|
|
131
|
+
@val external stdoutIsTty: Nullable.t<bool> = "process.stdout.isTTY"
|
|
132
|
+
|
|
133
|
+
let fork = (
|
|
134
|
+
worker: worker,
|
|
135
|
+
~workerIndex,
|
|
136
|
+
// How many processes the run's budgets are being split between.
|
|
137
|
+
~workerCount,
|
|
138
|
+
// Whether this worker waits for the run before going realtime. False when
|
|
139
|
+
// every chain resumed already caught up: there is nothing left to wait for,
|
|
140
|
+
// and a barrier nobody can open would hold the run forever.
|
|
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,
|
|
145
|
+
// The entry this process was itself started from, so a worker is the same
|
|
146
|
+
// program as its supervisor however the package was installed.
|
|
147
|
+
~entryPath=NodeJs.Process.argv->Array.getUnsafe(1),
|
|
148
|
+
// A run that draws a display reads its workers' output instead of letting
|
|
149
|
+
// them write to the terminal behind the frame's back.
|
|
150
|
+
~pipeOutput=false,
|
|
151
|
+
~onOutput=Console.log,
|
|
152
|
+
~onErrorOutput=Console.error,
|
|
153
|
+
~onSnapshot=() => (),
|
|
154
|
+
) => {
|
|
155
|
+
let env = NodeJs.Process.process.env->Dict.copy
|
|
156
|
+
env->Dict.set(
|
|
157
|
+
Worker.envVar,
|
|
158
|
+
{
|
|
159
|
+
Worker.chainIds: worker.chainIds,
|
|
160
|
+
holdRealtime,
|
|
161
|
+
isDev,
|
|
162
|
+
}->S.reverseConvertToJsonStringOrThrow(Worker.configSchema),
|
|
163
|
+
)
|
|
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.
|
|
166
|
+
env->Dict.set("ENVIO_PG_MAX_CONNECTIONS", worker.maxConnections->Int.toString)
|
|
167
|
+
memoryBudgets(~workerCount)->Array.forEach(((name, share)) => env->Dict.set(name, share))
|
|
168
|
+
env->Dict.set("LOG_FILE", logFilePath(~workerIndex))
|
|
169
|
+
if pipeOutput && stdoutIsTty->Nullable.toOption->Option.getOr(false) {
|
|
170
|
+
env->Dict.set("FORCE_COLOR", "1")
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let child = NodeJs.ChildProcess.fork(
|
|
174
|
+
entryPath,
|
|
175
|
+
[],
|
|
176
|
+
{
|
|
177
|
+
env,
|
|
178
|
+
serialization: "advanced",
|
|
179
|
+
stdio: pipeOutput
|
|
180
|
+
? ["inherit", "pipe", "pipe", "ipc"]
|
|
181
|
+
: ["inherit", "inherit", "inherit", "ipc"],
|
|
182
|
+
},
|
|
183
|
+
)
|
|
184
|
+
if pipeOutput {
|
|
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)) =>
|
|
193
|
+
switch stream->Null.toOption {
|
|
194
|
+
| Some(stream) => {
|
|
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)
|
|
199
|
+
}
|
|
200
|
+
| None => ()
|
|
201
|
+
}
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
let running = {worker, child, snapshot: None, runtime: None, settled: false}
|
|
205
|
+
child->NodeJs.ChildProcess.Child.onMessage(message =>
|
|
206
|
+
switch message {
|
|
207
|
+
| Worker.Snapshot({metrics, runtime}) => {
|
|
208
|
+
running.snapshot = Some(metrics)
|
|
209
|
+
running.runtime = Some(runtime)
|
|
210
|
+
onSnapshot()
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
)
|
|
214
|
+
running
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// The forked workers of one run, and whether their supervisor is the one
|
|
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
|
+
}
|
|
227
|
+
|
|
228
|
+
let stop = group => {
|
|
229
|
+
group.stopping = true
|
|
230
|
+
group.running->Array.forEach(r => r.child->NodeJs.ChildProcess.Child.kill("SIGTERM")->ignore)
|
|
231
|
+
}
|
|
232
|
+
|
|
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.
|
|
237
|
+
let syncCache = {
|
|
238
|
+
let inFlight = ref(None)
|
|
239
|
+
(~dump) =>
|
|
240
|
+
switch inFlight.contents {
|
|
241
|
+
| Some(dumping) => dumping
|
|
242
|
+
| None =>
|
|
243
|
+
let dumping = dump()->Promise.finally(() => inFlight := None)
|
|
244
|
+
inFlight := Some(dumping)
|
|
245
|
+
dumping
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// The supervisor handed its connections to the workers, so a dump opens one of
|
|
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.
|
|
253
|
+
let dumpCache = (~config) => {
|
|
254
|
+
let storage = PgStorage.makeStorageFromEnv(~config, ~sql=PgStorage.makeClient(~maxConnections=1))
|
|
255
|
+
storage.dumpEffectCache()->Promise.finally(() => storage.close()->Promise.ignore)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// How a group ended. `Finished` is every worker exiting cleanly on its own,
|
|
259
|
+
// which is what indexing to every end block looks like.
|
|
260
|
+
type outcome = Finished | Stopped
|
|
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
|
+
|
|
293
|
+
// Resolves once every worker has ended. Throws if any of them ended in a way
|
|
294
|
+
// the supervisor didn't ask for, having first taken the rest down: one worker
|
|
295
|
+
// short leaves its chains unindexed, and a run that kept the others going would
|
|
296
|
+
// look healthy while falling behind.
|
|
297
|
+
let awaitExit = async (group): outcome => {
|
|
298
|
+
let failed = ref(false)
|
|
299
|
+
let alive = ref(group.running->Array.length)
|
|
300
|
+
|
|
301
|
+
await Promise.make((resolve, _) => {
|
|
302
|
+
let onGone = (r, ~ending) =>
|
|
303
|
+
if !r.settled {
|
|
304
|
+
r.settled = true
|
|
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
|
|
312
|
+
group->stop
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
alive := alive.contents - 1
|
|
316
|
+
if alive.contents === 0 {
|
|
317
|
+
resolve()
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
group.running->Array.forEach(r => {
|
|
322
|
+
r.child->NodeJs.ChildProcess.Child.onExit(
|
|
323
|
+
(code, signal) => r->onGone(~ending=classifyExit(~code, ~signal, ~stopping=group.stopping)),
|
|
324
|
+
)
|
|
325
|
+
r.child->NodeJs.ChildProcess.Child.onError(
|
|
326
|
+
exn => {
|
|
327
|
+
Logging.errorWithExn(exn, `${r.worker->label} failed to start`)
|
|
328
|
+
r->onGone(~ending=Failed)
|
|
329
|
+
},
|
|
330
|
+
)
|
|
331
|
+
})
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
if failed.contents {
|
|
335
|
+
JsError.throwWithMessage("An indexer process exited with a failure. Stopped the others.")
|
|
336
|
+
}
|
|
337
|
+
group.stopping ? Stopped : Finished
|
|
338
|
+
}
|
|
339
|
+
|
|
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
|
|
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
|
+
})
|
|
422
|
+
|
|
423
|
+
// Runs the group: creates the schema for every chain, forks a worker per plan
|
|
424
|
+
// entry, and serves the run's metrics, console and display from what they
|
|
425
|
+
// report. Returns once every worker has exited; throws if any of them failed.
|
|
426
|
+
let run = async (~config: Config.t, ~workers: array<worker>, ~reset) => {
|
|
427
|
+
// Every chain's state has to exist before a worker resumes it: an isolated
|
|
428
|
+
// run refuses to initialize, precisely so it can't create rows for its own
|
|
429
|
+
// chains and leave the chains it skipped with nothing to resume. It is the
|
|
430
|
+
// same initialization an unsplit run does, and the supervisor hands the
|
|
431
|
+
// connections it used to its workers.
|
|
432
|
+
let persistence = PgStorage.makePersistenceFromConfig(~config)
|
|
433
|
+
await persistence->Persistence.initForRun(
|
|
434
|
+
~config,
|
|
435
|
+
~reset,
|
|
436
|
+
~isDevelopmentMode=config.isDev,
|
|
437
|
+
~requireInitialized=false,
|
|
438
|
+
)
|
|
439
|
+
await persistence.storage.close()
|
|
440
|
+
|
|
441
|
+
let startTime = Date.make()
|
|
442
|
+
let startTimeRef = Performance.now()
|
|
443
|
+
|
|
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
|
+
})
|
|
453
|
+
|
|
454
|
+
// Decided before the first fork: it is what makes a worker's output the
|
|
455
|
+
// supervisor's to print.
|
|
456
|
+
let shouldUseTui = Tui.shouldUse()
|
|
457
|
+
// A run that resumed with every chain already caught up owes nobody a wait:
|
|
458
|
+
// its workers start realtime and there is no barrier to open.
|
|
459
|
+
let holdRealtime =
|
|
460
|
+
(persistence->Persistence.getInitializedState).chains->Array.some(chain =>
|
|
461
|
+
chain.timestampCaughtUpToHeadOrEndblock->Option.isNone
|
|
462
|
+
)
|
|
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
|
+
)
|
|
475
|
+
|
|
476
|
+
let reported = () => group.running->Array.filterMap(r => r.snapshot)
|
|
477
|
+
let merge = snapshots =>
|
|
478
|
+
Metrics.merge(
|
|
479
|
+
snapshots,
|
|
480
|
+
~startTime,
|
|
481
|
+
~metricTime=Date.make(),
|
|
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(),
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
Server.startServer(
|
|
490
|
+
// Nothing to report until a worker has: the run reads as initializing
|
|
491
|
+
// rather than as an indexer with no chains.
|
|
492
|
+
~getMetrics=() =>
|
|
493
|
+
switch reported() {
|
|
494
|
+
| [] => None
|
|
495
|
+
| snapshots => Some(snapshots->merge)
|
|
496
|
+
},
|
|
497
|
+
~envioVersion=Utils.EnvioPackage.value.version,
|
|
498
|
+
// The workers' readings, each under a `worker` label: theirs are the memory
|
|
499
|
+
// and the event loop the indexing runs on.
|
|
500
|
+
~collectRuntime=() =>
|
|
501
|
+
Metrics.renderRuntime(
|
|
502
|
+
group.running->Array.filterMap(r =>
|
|
503
|
+
r.runtime->Option.map(runtime => (`worker="${r.worker->name}"`, runtime))
|
|
504
|
+
),
|
|
505
|
+
),
|
|
506
|
+
~isDevelopmentMode=config.isDev,
|
|
507
|
+
~onSyncCache=() => syncCache(~dump=() => dumpCache(~config)),
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
if shouldUseTui {
|
|
511
|
+
let _rerender = Tui.start(~config, ~getMetrics=() =>
|
|
512
|
+
switch reported() {
|
|
513
|
+
| [] => {...[]->merge, chains: configuredChains(config)}
|
|
514
|
+
| snapshots => snapshots->merge
|
|
515
|
+
}
|
|
516
|
+
)
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Whichever signal asks the run to stop, the supervisor is the one that
|
|
520
|
+
// stops the workers: an interrupt from the terminal reaches them too, but
|
|
521
|
+
// they leave it to the supervisor.
|
|
522
|
+
NodeJs.Process.onSignal("SIGTERM", () => group->stop)
|
|
523
|
+
NodeJs.Process.onSignal("SIGINT", () => group->stop)
|
|
524
|
+
|
|
525
|
+
// The server and the signal handlers would keep this process up after its
|
|
526
|
+
// last worker is gone, so the group's end has to end the process. A display
|
|
527
|
+
// is the exception, as it is for a single process: it keeps the final state
|
|
528
|
+
// on screen until the terminal closes it.
|
|
529
|
+
let outcome = await group->awaitExit
|
|
530
|
+
|
|
531
|
+
switch outcome {
|
|
532
|
+
| Stopped => NodeJs.process->NodeJs.exitWithCode(Success)
|
|
533
|
+
| Finished if !shouldUseTui =>
|
|
534
|
+
Logging.info("Exiting with success")
|
|
535
|
+
NodeJs.process->NodeJs.exitWithCode(Success)
|
|
536
|
+
| Finished =>
|
|
537
|
+
// With nothing left to stop, the stop signals end the display instead.
|
|
538
|
+
// Registering a handler above took over from Node's default exit.
|
|
539
|
+
NodeJs.Process.onSignal("SIGTERM", () => NodeJs.process->NodeJs.exitWithCode(Success))
|
|
540
|
+
NodeJs.Process.onSignal("SIGINT", () => NodeJs.process->NodeJs.exitWithCode(Success))
|
|
541
|
+
}
|
|
542
|
+
}
|