envio 3.12.1 → 3.13.0-alpha.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 -6
- package/src/BatchProcessing.res +9 -9
- package/src/BatchProcessing.res.mjs +5 -5
- package/src/Bin.res +24 -17
- package/src/Bin.res.mjs +5 -0
- package/src/ChainFetching.res +24 -10
- package/src/ChainFetching.res.mjs +8 -3
- package/src/ChainState.res +43 -0
- package/src/ChainState.res.mjs +43 -0
- package/src/ChainState.resi +2 -0
- package/src/Config.res +35 -0
- package/src/Config.res.mjs +39 -0
- package/src/Core.res +4 -0
- package/src/Core.res.mjs +4 -0
- package/src/CrossChainState.res +60 -3
- package/src/CrossChainState.res.mjs +38 -4
- package/src/CrossChainState.resi +10 -1
- package/src/Env.res +4 -0
- package/src/IndexerLoop.res +2 -0
- package/src/IndexerLoop.res.mjs +1 -0
- package/src/IndexerState.res +41 -1
- package/src/IndexerState.res.mjs +43 -4
- package/src/IndexerState.resi +10 -0
- package/src/Logging.res +38 -6
- package/src/Logging.res.mjs +32 -5
- package/src/Main.res +131 -268
- package/src/Main.res.mjs +28 -152
- package/src/Metrics.res +263 -102
- package/src/Metrics.res.mjs +227 -48
- package/src/Persistence.res +27 -2
- package/src/Persistence.res.mjs +9 -2
- package/src/PgStorage.res +17 -9
- package/src/PgStorage.res.mjs +11 -8
- package/src/Server.res +181 -0
- package/src/Server.res.mjs +143 -0
- package/src/Supervisor.res +415 -0
- package/src/Supervisor.res.mjs +325 -0
- package/src/TestIndexer.res.mjs +1 -1
- package/src/Worker.res +95 -0
- package/src/Worker.res.mjs +80 -0
- package/src/bindings/NodeJs.res +41 -0
- package/src/db/InternalTable.res +8 -1
- package/src/db/InternalTable.res.mjs +5 -1
- package/src/tui/Tui.res +24 -0
- package/src/tui/Tui.res.mjs +18 -0
- package/src/tui/components/SyncETA.res +12 -6
- package/src/tui/components/SyncETA.res.mjs +12 -8
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Env from "./Env.res.mjs";
|
|
4
|
+
import * as Tui from "./tui/Tui.res.mjs";
|
|
5
|
+
import * as Utils from "./Utils.res.mjs";
|
|
6
|
+
import * as Config from "./Config.res.mjs";
|
|
7
|
+
import * as Server from "./Server.res.mjs";
|
|
8
|
+
import * as Worker from "./Worker.res.mjs";
|
|
9
|
+
import * as ChainId from "./ChainId.res.mjs";
|
|
10
|
+
import * as Logging from "./Logging.res.mjs";
|
|
11
|
+
import * as Metrics from "./Metrics.res.mjs";
|
|
12
|
+
import * as Process from "process";
|
|
13
|
+
import * as ChainMap from "./ChainMap.res.mjs";
|
|
14
|
+
import * as PgStorage from "./PgStorage.res.mjs";
|
|
15
|
+
import * as Performance from "./bindings/Performance.res.mjs";
|
|
16
|
+
import * as Persistence from "./Persistence.res.mjs";
|
|
17
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
18
|
+
import * as Primitive_int from "@rescript/runtime/lib/es6/Primitive_int.js";
|
|
19
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
20
|
+
import * as Child_process from "child_process";
|
|
21
|
+
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
22
|
+
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
23
|
+
import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs";
|
|
24
|
+
|
|
25
|
+
function plan(chainIds, maxConnections) {
|
|
26
|
+
let workerCount = Primitive_int.min(chainIds.length, maxConnections / 2 | 0);
|
|
27
|
+
if (workerCount < 2) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
let evenShare = Primitive_int.div(maxConnections, workerCount);
|
|
31
|
+
let remainder = Primitive_int.mod_(maxConnections, workerCount);
|
|
32
|
+
return Stdlib_Array.fromInitializer(workerCount, workerIndex => ({
|
|
33
|
+
chainIds: chainIds.filter((param, dealIndex) => {
|
|
34
|
+
let position = Primitive_int.mod_(dealIndex, workerCount);
|
|
35
|
+
let isReversePass = Primitive_int.div(dealIndex, workerCount) % 2 === 1;
|
|
36
|
+
return (
|
|
37
|
+
isReversePass ? (workerCount - 1 | 0) - position | 0 : position
|
|
38
|
+
) === workerIndex;
|
|
39
|
+
}),
|
|
40
|
+
maxConnections: evenShare + (
|
|
41
|
+
workerIndex < remainder ? 1 : 0
|
|
42
|
+
) | 0
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function planForRun(config, maxConnectionsOpt) {
|
|
47
|
+
let maxConnections = maxConnectionsOpt !== undefined ? maxConnectionsOpt : Env.Db.maxConnections;
|
|
48
|
+
if (config.isolated || !Config.isPerChain(config)) {
|
|
49
|
+
return;
|
|
50
|
+
} else {
|
|
51
|
+
return plan(ChainMap.values(config.chainMap).map(chain => chain.id), maxConnections);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function name(worker) {
|
|
56
|
+
return worker.chainIds.map(ChainId.toString).join(";");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function label(worker) {
|
|
60
|
+
return `[chain ` + name(worker) + `]`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function logFilePath(workerIndex, pathOpt) {
|
|
64
|
+
let path = pathOpt !== undefined ? pathOpt : Env.logFilePath;
|
|
65
|
+
let suffix = `.worker-` + workerIndex.toString();
|
|
66
|
+
let dot = path.lastIndexOf(".");
|
|
67
|
+
if (dot > path.lastIndexOf("/")) {
|
|
68
|
+
return path.slice(0, dot) + suffix + path.slice(dot, path.length);
|
|
69
|
+
} else {
|
|
70
|
+
return path + suffix;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function readLines(onLine) {
|
|
75
|
+
let pending = {
|
|
76
|
+
contents: ""
|
|
77
|
+
};
|
|
78
|
+
let read = chunk => {
|
|
79
|
+
let parts = (pending.contents + chunk).split("\n");
|
|
80
|
+
pending.contents = Stdlib_Option.getOr(parts.pop(), "");
|
|
81
|
+
parts.forEach(onLine);
|
|
82
|
+
};
|
|
83
|
+
let flush = () => {
|
|
84
|
+
let line = pending.contents;
|
|
85
|
+
if (line === "") {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
pending.contents = "";
|
|
89
|
+
onLine(line);
|
|
90
|
+
};
|
|
91
|
+
return [
|
|
92
|
+
read,
|
|
93
|
+
flush
|
|
94
|
+
];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function fork(worker, workerIndex, holdRealtime, entryPathOpt, pipeOutputOpt, onOutputOpt) {
|
|
98
|
+
let entryPath = entryPathOpt !== undefined ? entryPathOpt : process.argv[1];
|
|
99
|
+
let pipeOutput = pipeOutputOpt !== undefined ? pipeOutputOpt : false;
|
|
100
|
+
let onOutput = onOutputOpt !== undefined ? onOutputOpt : prim => {
|
|
101
|
+
console.log(prim);
|
|
102
|
+
};
|
|
103
|
+
let env = Object.assign({}, Process.env);
|
|
104
|
+
env[Worker.envVar] = S$RescriptSchema.reverseConvertToJsonStringOrThrow({
|
|
105
|
+
chainIds: worker.chainIds,
|
|
106
|
+
holdRealtime: holdRealtime
|
|
107
|
+
}, Worker.configSchema, undefined);
|
|
108
|
+
env["ENVIO_PG_MAX_CONNECTIONS"] = worker.maxConnections.toString();
|
|
109
|
+
env["LOG_FILE"] = logFilePath(workerIndex, undefined);
|
|
110
|
+
if (pipeOutput && Stdlib_Option.getOr((process.stdout.isTTY == null) ? undefined : Primitive_option.some(process.stdout.isTTY), false)) {
|
|
111
|
+
env["FORCE_COLOR"] = "1";
|
|
112
|
+
}
|
|
113
|
+
let child = Child_process.fork(entryPath, [], {
|
|
114
|
+
env: env,
|
|
115
|
+
serialization: "advanced",
|
|
116
|
+
stdio: pipeOutput ? [
|
|
117
|
+
"inherit",
|
|
118
|
+
"pipe",
|
|
119
|
+
"pipe",
|
|
120
|
+
"ipc"
|
|
121
|
+
] : [
|
|
122
|
+
"inherit",
|
|
123
|
+
"inherit",
|
|
124
|
+
"inherit",
|
|
125
|
+
"ipc"
|
|
126
|
+
]
|
|
127
|
+
});
|
|
128
|
+
if (pipeOutput) {
|
|
129
|
+
[
|
|
130
|
+
child.stdout,
|
|
131
|
+
child.stderr
|
|
132
|
+
].forEach(stream => {
|
|
133
|
+
if (stream === null) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
let match = readLines(onOutput);
|
|
137
|
+
stream.setEncoding("utf8");
|
|
138
|
+
stream.on("data", match[0]);
|
|
139
|
+
stream.on("end", match[1]);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
let running = {
|
|
143
|
+
worker: worker,
|
|
144
|
+
child: child,
|
|
145
|
+
snapshot: undefined,
|
|
146
|
+
runtime: undefined,
|
|
147
|
+
settled: false
|
|
148
|
+
};
|
|
149
|
+
child.on("message", message => {
|
|
150
|
+
running.snapshot = message.metrics;
|
|
151
|
+
running.runtime = message.runtime;
|
|
152
|
+
});
|
|
153
|
+
return running;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function stop(group) {
|
|
157
|
+
group.stopping = true;
|
|
158
|
+
group.running.forEach(r => {
|
|
159
|
+
r.child.kill("SIGTERM");
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let inFlight = {
|
|
164
|
+
contents: undefined
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
function syncCache(dump) {
|
|
168
|
+
let dumping = inFlight.contents;
|
|
169
|
+
if (dumping !== undefined) {
|
|
170
|
+
return dumping;
|
|
171
|
+
}
|
|
172
|
+
let dumping$1 = dump().finally(() => {
|
|
173
|
+
inFlight.contents = undefined;
|
|
174
|
+
});
|
|
175
|
+
inFlight.contents = dumping$1;
|
|
176
|
+
return dumping$1;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function dumpCache(config) {
|
|
180
|
+
let storage = PgStorage.makeStorageFromEnv(config, Primitive_option.some(PgStorage.makeClient(1)), undefined, undefined);
|
|
181
|
+
return storage.dumpEffectCache().finally(() => {
|
|
182
|
+
storage.close();
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function awaitExit(group) {
|
|
187
|
+
let failed = {
|
|
188
|
+
contents: false
|
|
189
|
+
};
|
|
190
|
+
let alive = {
|
|
191
|
+
contents: group.running.length
|
|
192
|
+
};
|
|
193
|
+
await new Promise((resolve, param) => {
|
|
194
|
+
let onGone = (r, failure) => {
|
|
195
|
+
if (!r.settled) {
|
|
196
|
+
r.settled = true;
|
|
197
|
+
if (failure) {
|
|
198
|
+
failed.contents = true;
|
|
199
|
+
if (!group.stopping) {
|
|
200
|
+
stop(group);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
alive.contents = alive.contents - 1 | 0;
|
|
204
|
+
if (alive.contents === 0) {
|
|
205
|
+
return resolve();
|
|
206
|
+
} else {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
group.running.forEach(r => {
|
|
212
|
+
r.child.on("exit", (code, _signal) => onGone(r, !group.stopping && (
|
|
213
|
+
code === null ? undefined : Primitive_option.some(code)
|
|
214
|
+
) !== 0));
|
|
215
|
+
r.child.on("error", exn => {
|
|
216
|
+
Logging.errorWithExn(exn, label(r.worker) + ` failed to start`);
|
|
217
|
+
onGone(r, true);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
if (failed.contents) {
|
|
222
|
+
Stdlib_JsError.throwWithMessage("An indexer process exited with a failure. Stopped the others.");
|
|
223
|
+
}
|
|
224
|
+
if (group.stopping) {
|
|
225
|
+
return "Stopped";
|
|
226
|
+
} else {
|
|
227
|
+
return "Finished";
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function isRunAtHead(snapshots, workerCount) {
|
|
232
|
+
if (snapshots.length === workerCount) {
|
|
233
|
+
return snapshots.every(snapshot => snapshot.hasArrivedAtHead);
|
|
234
|
+
} else {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function run(config, workers, reset) {
|
|
240
|
+
let persistence = PgStorage.makePersistenceFromConfig(config, undefined);
|
|
241
|
+
await Persistence.initForRun(persistence, config, reset, config.isDev, false);
|
|
242
|
+
await persistence.storage.close();
|
|
243
|
+
let startTime = new Date();
|
|
244
|
+
let startTimeRef = Performance.now();
|
|
245
|
+
Logging.info(`Splitting ` + ChainMap.values(config.chainMap).length.toString() + ` chains across ` + workers.length.toString() + ` processes, from a budget of ` + Env.Db.maxConnections.toString() + ` database connections.`);
|
|
246
|
+
let shouldUseTui = Tui.shouldUse(undefined);
|
|
247
|
+
let holdRealtime = Persistence.getInitializedState(persistence).chains.some(chain => Stdlib_Option.isNone(chain.timestampCaughtUpToHeadOrEndblock));
|
|
248
|
+
let group = {
|
|
249
|
+
running: workers.map((worker, workerIndex) => fork(worker, workerIndex, holdRealtime, undefined, shouldUseTui, undefined)),
|
|
250
|
+
stopping: false
|
|
251
|
+
};
|
|
252
|
+
let reported = () => Stdlib_Array.filterMap(group.running, r => r.snapshot);
|
|
253
|
+
let merge = snapshots => Metrics.merge(snapshots, startTime, new Date(), Performance.secondsSince(startTimeRef));
|
|
254
|
+
Server.startServer(() => {
|
|
255
|
+
let snapshots = reported();
|
|
256
|
+
if (snapshots.length !== 0) {
|
|
257
|
+
return merge(snapshots);
|
|
258
|
+
}
|
|
259
|
+
}, Utils.EnvioPackage.value.version, () => syncCache(() => dumpCache(config)), () => Metrics.renderRuntime(Stdlib_Array.filterMap(group.running, r => Stdlib_Option.map(r.runtime, runtime => [
|
|
260
|
+
`worker="` + name(r.worker) + `"`,
|
|
261
|
+
runtime
|
|
262
|
+
]))), config.isDev);
|
|
263
|
+
let releaseCheck = {
|
|
264
|
+
contents: undefined
|
|
265
|
+
};
|
|
266
|
+
let stopReleaseCheck = () => {
|
|
267
|
+
Stdlib_Option.forEach(releaseCheck.contents, prim => {
|
|
268
|
+
clearInterval(prim);
|
|
269
|
+
});
|
|
270
|
+
releaseCheck.contents = undefined;
|
|
271
|
+
};
|
|
272
|
+
if (holdRealtime) {
|
|
273
|
+
releaseCheck.contents = Primitive_option.some(setInterval(() => {
|
|
274
|
+
if (isRunAtHead(reported(), group.running.length)) {
|
|
275
|
+
stopReleaseCheck();
|
|
276
|
+
group.running.forEach(r => {
|
|
277
|
+
r.child.send("release-realtime");
|
|
278
|
+
});
|
|
279
|
+
return Logging.info("Every chain has reached the head. Switching the run to realtime.");
|
|
280
|
+
}
|
|
281
|
+
}, 500));
|
|
282
|
+
}
|
|
283
|
+
if (shouldUseTui) {
|
|
284
|
+
Tui.start(config, () => merge(reported()));
|
|
285
|
+
}
|
|
286
|
+
process.on("SIGTERM", () => stop(group));
|
|
287
|
+
process.on("SIGINT", () => stop(group));
|
|
288
|
+
let outcome = await awaitExit(group);
|
|
289
|
+
stopReleaseCheck();
|
|
290
|
+
if (outcome === "Finished") {
|
|
291
|
+
if (shouldUseTui) {
|
|
292
|
+
process.on("SIGTERM", () => {
|
|
293
|
+
Process.exit(0);
|
|
294
|
+
});
|
|
295
|
+
process.on("SIGINT", () => {
|
|
296
|
+
Process.exit(0);
|
|
297
|
+
});
|
|
298
|
+
} else {
|
|
299
|
+
Logging.info("Exiting with success");
|
|
300
|
+
Process.exit(0);
|
|
301
|
+
}
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
Process.exit(0);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
let minConnectionsPerWorker = 2;
|
|
308
|
+
|
|
309
|
+
export {
|
|
310
|
+
minConnectionsPerWorker,
|
|
311
|
+
plan,
|
|
312
|
+
planForRun,
|
|
313
|
+
name,
|
|
314
|
+
label,
|
|
315
|
+
logFilePath,
|
|
316
|
+
readLines,
|
|
317
|
+
fork,
|
|
318
|
+
stop,
|
|
319
|
+
syncCache,
|
|
320
|
+
dumpCache,
|
|
321
|
+
awaitExit,
|
|
322
|
+
isRunAtHead,
|
|
323
|
+
run,
|
|
324
|
+
}
|
|
325
|
+
/* Env Not a pure module */
|
package/src/TestIndexer.res.mjs
CHANGED
|
@@ -639,7 +639,7 @@ function createTestIndexer() {
|
|
|
639
639
|
};
|
|
640
640
|
try {
|
|
641
641
|
await new Promise((resolve, reject) => {
|
|
642
|
-
let indexerState = IndexerState.makeFromDbState(runConfig, persistence, initialState, registrationsByChainId, undefined, undefined, exitAfterFirstEventBlock, undefined, undefined, errHandler => {
|
|
642
|
+
let indexerState = IndexerState.makeFromDbState(runConfig, persistence, initialState, registrationsByChainId, undefined, undefined, exitAfterFirstEventBlock, undefined, undefined, undefined, errHandler => {
|
|
643
643
|
ErrorHandling.log(errHandler);
|
|
644
644
|
reject(Utils.prettifyExn(errHandler.exn));
|
|
645
645
|
}, () => resolve());
|
package/src/Worker.res
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// The worker side of a supervised run: a process the supervisor forked to drive
|
|
2
|
+
// a subset of the chains. It has no server and no TUI of its own — it reports
|
|
3
|
+
// through the IPC channel, and the supervisor is the one operational surface.
|
|
4
|
+
|
|
5
|
+
// Set by a supervisor in the environment of the workers it forks. Internal:
|
|
6
|
+
// it counts only together with the fork's own channel, so a copy left in a
|
|
7
|
+
// shell starts nothing, and an indexer a user starts themselves takes every
|
|
8
|
+
// path it takes today.
|
|
9
|
+
let envVar = "ENVIO_INTERNAL_WORKER"
|
|
10
|
+
|
|
11
|
+
// What the supervisor decided about this worker, handed over in the spawn
|
|
12
|
+
// environment rather than over the channel: it is settled before the process
|
|
13
|
+
// starts, and the worker needs it before it can load its own config.
|
|
14
|
+
type config = {
|
|
15
|
+
chainIds: array<ChainId.t>,
|
|
16
|
+
// The chains this worker drives may reach the head while chains in another
|
|
17
|
+
// process are still backfilling, and an indexer goes realtime as a whole or
|
|
18
|
+
// not at all. Cleared by the supervisor's `ReleaseRealtime`.
|
|
19
|
+
holdRealtime: bool,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let configSchema = S.object((s): config => {
|
|
23
|
+
chainIds: s.field("chainIds", S.array(ChainId.schema)),
|
|
24
|
+
holdRealtime: s.fieldOr("holdRealtime", S.bool, false),
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
// Read as this module loads, which is before anything that could catch a bare
|
|
28
|
+
// schema error and say where it came from.
|
|
29
|
+
let detect = (~env: dict<string>, ~hasChannel) =>
|
|
30
|
+
switch (hasChannel, env->Dict.get(envVar)) {
|
|
31
|
+
| (true, Some(json)) =>
|
|
32
|
+
switch json->S.parseJsonStringOrThrow(configSchema) {
|
|
33
|
+
| config => Some(config)
|
|
34
|
+
| exception S.Raised(error) =>
|
|
35
|
+
JsError.throwWithMessage(
|
|
36
|
+
`Invalid ${envVar}: ${error->S.Error.message}. It is set by an indexer supervisor for the processes it forks, and isn't meant to be set by hand.`,
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
| _ => None
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let config = detect(
|
|
43
|
+
~env=NodeJs.Process.process.env,
|
|
44
|
+
~hasChannel=NodeJs.Process.channel->Nullable.toOption->Option.isSome,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
let isEnabled = config->Option.isSome
|
|
48
|
+
|
|
49
|
+
@tag("kind")
|
|
50
|
+
type parentMessage =
|
|
51
|
+
// Every chain in the run has reached the head, so this worker may enter the
|
|
52
|
+
// reorg threshold and switch to realtime with the rest of them.
|
|
53
|
+
| @as("release-realtime") ReleaseRealtime
|
|
54
|
+
|
|
55
|
+
@tag("kind")
|
|
56
|
+
type workerMessage =
|
|
57
|
+
| @as("snapshot") Snapshot({metrics: Metrics.t, runtime: Metrics.runtimeSample})
|
|
58
|
+
|
|
59
|
+
// How often a worker reports. Matches the TUI's own refresh, so the supervised
|
|
60
|
+
// display moves at the same rate an unsplit run's does.
|
|
61
|
+
%%private(let snapshotIntervalMillis = 500)
|
|
62
|
+
|
|
63
|
+
// The supervisor is the one that stops a worker, and the one whose absence
|
|
64
|
+
// ends it. A terminal's interrupt reaches the whole group at once, so the
|
|
65
|
+
// worker leaves it to the supervisor, which stops every worker in turn; without
|
|
66
|
+
// that, a worker gone on its own would read as a failure to the supervisor
|
|
67
|
+
// still deciding what the interrupt meant. A supervisor that dies can't tear
|
|
68
|
+
// the group down, so losing the channel is what ends the worker then.
|
|
69
|
+
let bindToSupervisor = () => {
|
|
70
|
+
NodeJs.Process.onSignal("SIGINT", () => ())
|
|
71
|
+
NodeJs.Process.onDisconnect(() => {
|
|
72
|
+
Logging.error("The indexer supervisor is gone. Stopping this chain's process.")
|
|
73
|
+
NodeJs.process->NodeJs.exitWithCode(Failure)
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
%%private(let send = (message: workerMessage) => NodeJs.Process.sendToParent(message)->ignore)
|
|
78
|
+
|
|
79
|
+
// Reports this process's chains and its own runtime for as long as it runs, so
|
|
80
|
+
// the supervisor can merge every worker's into the one snapshot the run serves,
|
|
81
|
+
// and listens for the one decision the supervisor makes on the run's behalf.
|
|
82
|
+
// Does nothing in a process nobody forked.
|
|
83
|
+
let bindRun = (~getMetrics: unit => Metrics.t, ~onReleaseRealtime: unit => unit) =>
|
|
84
|
+
if isEnabled {
|
|
85
|
+
Metrics.startRuntimeCollectors()
|
|
86
|
+
let _intervalId = setInterval(
|
|
87
|
+
() => send(Snapshot({metrics: getMetrics(), runtime: Metrics.sampleRuntime()})),
|
|
88
|
+
snapshotIntervalMillis,
|
|
89
|
+
)
|
|
90
|
+
NodeJs.Process.onMessage((message: parentMessage) =>
|
|
91
|
+
switch message {
|
|
92
|
+
| ReleaseRealtime => onReleaseRealtime()
|
|
93
|
+
}
|
|
94
|
+
)
|
|
95
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as ChainId from "./ChainId.res.mjs";
|
|
4
|
+
import * as Logging from "./Logging.res.mjs";
|
|
5
|
+
import * as Metrics from "./Metrics.res.mjs";
|
|
6
|
+
import * as Process from "process";
|
|
7
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
8
|
+
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
9
|
+
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
10
|
+
import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs";
|
|
11
|
+
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
12
|
+
|
|
13
|
+
let envVar = "ENVIO_INTERNAL_WORKER";
|
|
14
|
+
|
|
15
|
+
let configSchema = S$RescriptSchema.object(s => ({
|
|
16
|
+
chainIds: s.f("chainIds", S$RescriptSchema.array(ChainId.schema)),
|
|
17
|
+
holdRealtime: s.fieldOr("holdRealtime", S$RescriptSchema.bool, false)
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
function detect(env, hasChannel) {
|
|
21
|
+
let match = env[envVar];
|
|
22
|
+
if (!hasChannel) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (match === undefined) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
let config;
|
|
29
|
+
try {
|
|
30
|
+
config = S$RescriptSchema.parseJsonStringOrThrow(match, configSchema);
|
|
31
|
+
} catch (raw_error) {
|
|
32
|
+
let error = Primitive_exceptions.internalToException(raw_error);
|
|
33
|
+
if (error.RE_EXN_ID === S$RescriptSchema.Raised) {
|
|
34
|
+
return Stdlib_JsError.throwWithMessage(`Invalid ` + envVar + `: ` + S$RescriptSchema.$$Error.message(error._1) + `. It is set by an indexer supervisor for the processes it forks, and isn't meant to be set by hand.`);
|
|
35
|
+
}
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
return config;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let config = detect(Process.env, Stdlib_Option.isSome(Primitive_option.fromNullable(process.channel)));
|
|
42
|
+
|
|
43
|
+
let isEnabled = Stdlib_Option.isSome(config);
|
|
44
|
+
|
|
45
|
+
function bindToSupervisor() {
|
|
46
|
+
process.on("SIGINT", () => {});
|
|
47
|
+
process.on("disconnect", () => {
|
|
48
|
+
Logging.error("The indexer supervisor is gone. Stopping this chain's process.");
|
|
49
|
+
Process.exit(1);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function bindRun(getMetrics, onReleaseRealtime) {
|
|
54
|
+
if (isEnabled) {
|
|
55
|
+
Metrics.startRuntimeCollectors();
|
|
56
|
+
setInterval(() => {
|
|
57
|
+
let message_0 = getMetrics();
|
|
58
|
+
let message_1 = Metrics.sampleRuntime();
|
|
59
|
+
let message = {
|
|
60
|
+
kind: "snapshot",
|
|
61
|
+
metrics: message_0,
|
|
62
|
+
runtime: message_1
|
|
63
|
+
};
|
|
64
|
+
process.send(message);
|
|
65
|
+
}, 500);
|
|
66
|
+
process.on("message", message => onReleaseRealtime());
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export {
|
|
72
|
+
envVar,
|
|
73
|
+
configSchema,
|
|
74
|
+
detect,
|
|
75
|
+
config,
|
|
76
|
+
isEnabled,
|
|
77
|
+
bindToSupervisor,
|
|
78
|
+
bindRun,
|
|
79
|
+
}
|
|
80
|
+
/* configSchema Not a pure module */
|
package/src/bindings/NodeJs.res
CHANGED
|
@@ -64,6 +64,21 @@ module Process = {
|
|
|
64
64
|
@module("process") external version: string = "version"
|
|
65
65
|
@module("process")
|
|
66
66
|
external getActiveResourcesInfo: unit => array<string> = "getActiveResourcesInfo"
|
|
67
|
+
|
|
68
|
+
// Only a process forked with an IPC channel has these. Called through
|
|
69
|
+
// `process` rather than off a namespace import, which would drop the
|
|
70
|
+
// receiver Node's own implementations read.
|
|
71
|
+
@val @scope("process") external sendToParent: 'msg => bool = "send"
|
|
72
|
+
@val @scope("process")
|
|
73
|
+
external onMessage: (@as("message") _, 'msg => unit) => unit = "on"
|
|
74
|
+
@val @scope("process") external onSignal: (string, unit => unit) => unit = "on"
|
|
75
|
+
// Present only in a process forked with an IPC channel.
|
|
76
|
+
@val @scope("process") external channel: Nullable.t<unknown> = "channel"
|
|
77
|
+
@val @scope("process")
|
|
78
|
+
external onDisconnect: (@as("disconnect") _, unit => unit) => unit = "on"
|
|
79
|
+
@val @scope("process") external argv: array<string> = "argv"
|
|
80
|
+
@val @scope("process")
|
|
81
|
+
external emitMessage: (@as("message") _, 'msg) => bool = "emit"
|
|
67
82
|
}
|
|
68
83
|
|
|
69
84
|
module Buffer = {
|
|
@@ -141,6 +156,32 @@ module ChildProcess = {
|
|
|
141
156
|
|
|
142
157
|
@module("child_process")
|
|
143
158
|
external execWithOptions: (string, execOptions, callback) => unit = "exec"
|
|
159
|
+
|
|
160
|
+
type child
|
|
161
|
+
type forkOptions = {
|
|
162
|
+
cwd?: string,
|
|
163
|
+
env?: dict<string>,
|
|
164
|
+
// "advanced" uses the structured clone algorithm, so a message keeps the
|
|
165
|
+
// Date values a metrics snapshot carries instead of stringifying them.
|
|
166
|
+
serialization?: string,
|
|
167
|
+
stdio?: array<string>,
|
|
168
|
+
}
|
|
169
|
+
@module("child_process")
|
|
170
|
+
external fork: (string, array<string>, forkOptions) => child = "fork"
|
|
171
|
+
@send external send: (child, 'msg) => bool = "send"
|
|
172
|
+
@send external onMessage: (child, @as("message") _, 'msg => unit) => unit = "on"
|
|
173
|
+
@send
|
|
174
|
+
external onExit: (child, @as("exit") _, (Null.t<int>, Null.t<string>) => unit) => unit = "on"
|
|
175
|
+
@send external onChildError: (child, @as("error") _, exn => unit) => unit = "on"
|
|
176
|
+
@send external kill: (child, string) => bool = "kill"
|
|
177
|
+
|
|
178
|
+
// Present only for a stdio slot the parent asked to pipe.
|
|
179
|
+
type stdioStream
|
|
180
|
+
@get external stdout: child => Null.t<stdioStream> = "stdout"
|
|
181
|
+
@get external stderr: child => Null.t<stdioStream> = "stderr"
|
|
182
|
+
@send external setEncoding: (stdioStream, string) => unit = "setEncoding"
|
|
183
|
+
@send external onData: (stdioStream, @as("data") _, string => unit) => unit = "on"
|
|
184
|
+
@send external onEnd: (stdioStream, @as("end") _, unit => unit) => unit = "on"
|
|
144
185
|
}
|
|
145
186
|
|
|
146
187
|
module Url = {
|
package/src/db/InternalTable.res
CHANGED
|
@@ -344,7 +344,14 @@ VALUES ${valuesRows->Array.joinUnsafe(",\n ")};`,
|
|
|
344
344
|
let setClauses = Array.mapWithIndex(metaFields, (field, index) => {
|
|
345
345
|
let fieldName = (field :> string)
|
|
346
346
|
let paramIndex = index + 2 // +2 because $1 is for id in WHERE clause
|
|
347
|
-
|
|
347
|
+
switch field {
|
|
348
|
+
// A chain that caught up never un-catches up, so a metadata write staged
|
|
349
|
+
// before `markReady` and flushed after the stamp must not clear it. The
|
|
350
|
+
// writes race: metadata is written on a throttle of its own, outside the
|
|
351
|
+
// batch the finalization flushes.
|
|
352
|
+
| #ready_at => `"${fieldName}" = COALESCE($${Int.toString(paramIndex)}, "${fieldName}")`
|
|
353
|
+
| _ => `"${fieldName}" = $${Int.toString(paramIndex)}`
|
|
354
|
+
}
|
|
348
355
|
})
|
|
349
356
|
|
|
350
357
|
`UPDATE "${pgSchema}"."${table.tableName}"
|
|
@@ -246,7 +246,11 @@ let metaFields = [
|
|
|
246
246
|
function makeMetaFieldsUpdateQuery(pgSchema) {
|
|
247
247
|
let setClauses = metaFields.map((field, index) => {
|
|
248
248
|
let paramIndex = index + 2 | 0;
|
|
249
|
-
|
|
249
|
+
if (field === "ready_at") {
|
|
250
|
+
return `"` + field + `" = COALESCE($` + paramIndex.toString() + `, "` + field + `")`;
|
|
251
|
+
} else {
|
|
252
|
+
return `"` + field + `" = $` + paramIndex.toString();
|
|
253
|
+
}
|
|
250
254
|
});
|
|
251
255
|
return `UPDATE "` + pgSchema + `"."` + table$2.tableName + `"
|
|
252
256
|
SET ` + setClauses.join(",\n ") + `
|
package/src/tui/Tui.res
CHANGED
|
@@ -248,6 +248,30 @@ module App = {
|
|
|
248
248
|
}
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
+
type args = {@as("tui-off") tuiOff?: bool}
|
|
252
|
+
|
|
253
|
+
type process
|
|
254
|
+
@val external process: process = "process"
|
|
255
|
+
@get external argv: process => 'a = "argv"
|
|
256
|
+
|
|
257
|
+
type mainArgs = Yargs.parsedArgs<args>
|
|
258
|
+
|
|
259
|
+
// Whether this process draws the progress display: `--tui-off` first, then
|
|
260
|
+
// `ENVIO_TUI`, then whether anything is watching. A supervisor asks the same
|
|
261
|
+
// question its workers would have, since it is the one drawing for the run.
|
|
262
|
+
let shouldUse = (~suppressed=false) => {
|
|
263
|
+
let mainArgs: mainArgs = process->argv->Yargs.hideBin->Yargs.yargs->Yargs.argv
|
|
264
|
+
let explicitTui = switch mainArgs.tuiOff {
|
|
265
|
+
| Some(off) => Some(!off)
|
|
266
|
+
| None => Env.tuiEnvVar
|
|
267
|
+
}
|
|
268
|
+
switch (suppressed, explicitTui) {
|
|
269
|
+
| (true, _) => false
|
|
270
|
+
| (_, Some(tui)) => tui
|
|
271
|
+
| (_, None) => !Envio.isNonInteractive()
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
251
275
|
let start = (~config, ~getMetrics) => {
|
|
252
276
|
let {rerender} = render(<App config getMetrics />)
|
|
253
277
|
() => {
|
package/src/tui/Tui.res.mjs
CHANGED
|
@@ -3,14 +3,17 @@
|
|
|
3
3
|
import * as Env from "../Env.res.mjs";
|
|
4
4
|
import * as Ink from "./bindings/Ink.res.mjs";
|
|
5
5
|
import * as Ink$1 from "ink";
|
|
6
|
+
import * as Envio from "../Envio.res.mjs";
|
|
6
7
|
import * as React from "react";
|
|
7
8
|
import * as SyncETA from "./components/SyncETA.res.mjs";
|
|
8
9
|
import * as TuiData from "./components/TuiData.res.mjs";
|
|
9
10
|
import * as Messages from "./components/Messages.res.mjs";
|
|
11
|
+
import Yargs from "yargs/yargs";
|
|
10
12
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
11
13
|
import InkBigText from "ink-big-text";
|
|
12
14
|
import * as Primitive_int from "@rescript/runtime/lib/es6/Primitive_int.js";
|
|
13
15
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
16
|
+
import * as Helpers from "yargs/helpers";
|
|
14
17
|
import * as Primitive_float from "@rescript/runtime/lib/es6/Primitive_float.js";
|
|
15
18
|
import * as JsxRuntime from "react/jsx-runtime";
|
|
16
19
|
import * as BufferedProgressBar from "./components/BufferedProgressBar.res.mjs";
|
|
@@ -322,6 +325,20 @@ let App = {
|
|
|
322
325
|
make: Tui$App
|
|
323
326
|
};
|
|
324
327
|
|
|
328
|
+
function shouldUse(suppressedOpt) {
|
|
329
|
+
let suppressed = suppressedOpt !== undefined ? suppressedOpt : false;
|
|
330
|
+
let mainArgs = Yargs(Helpers.hideBin(process.argv)).argv;
|
|
331
|
+
let off = mainArgs["tui-off"];
|
|
332
|
+
let explicitTui = off !== undefined ? !off : Env.tuiEnvVar;
|
|
333
|
+
if (suppressed) {
|
|
334
|
+
return false;
|
|
335
|
+
} else if (explicitTui !== undefined) {
|
|
336
|
+
return explicitTui;
|
|
337
|
+
} else {
|
|
338
|
+
return !Envio.isNonInteractive();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
325
342
|
function start(config, getMetrics) {
|
|
326
343
|
let match = Ink.render(undefined, JsxRuntime.jsx(Tui$App, {
|
|
327
344
|
config: config,
|
|
@@ -339,6 +356,7 @@ export {
|
|
|
339
356
|
EventsPerSecond,
|
|
340
357
|
TotalEventsProcessed,
|
|
341
358
|
App,
|
|
359
|
+
shouldUse,
|
|
342
360
|
start,
|
|
343
361
|
}
|
|
344
362
|
/* Env Not a pure module */
|