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
package/src/Server.res
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// The indexer's own HTTP surface: metrics for a scraper, health for an
|
|
2
|
+
// orchestrator, and the console's view of the run. What it serves is handed to
|
|
3
|
+
// it, so one process's readings and a supervised group's merged ones render the
|
|
4
|
+
// same way.
|
|
5
|
+
|
|
6
|
+
// The public console/state chain shape. Kept to exactly this field set for
|
|
7
|
+
// backward compatibility with consumers like RACE — new metric fields stay off
|
|
8
|
+
// the HTTP response.
|
|
9
|
+
type chainData = {
|
|
10
|
+
chainId: ChainId.t,
|
|
11
|
+
poweredByHyperSync: bool,
|
|
12
|
+
firstEventBlockNumber: option<int>,
|
|
13
|
+
latestProcessedBlock: option<int>,
|
|
14
|
+
timestampCaughtUpToHeadOrEndblock: option<Date.t>,
|
|
15
|
+
numEventsProcessed: float,
|
|
16
|
+
latestFetchedBlockNumber: int,
|
|
17
|
+
// Need this for API backwards compatibility
|
|
18
|
+
@as("currentBlockHeight")
|
|
19
|
+
knownHeight: int,
|
|
20
|
+
numBatchesFetched: int,
|
|
21
|
+
startBlock: int,
|
|
22
|
+
endBlock: option<int>,
|
|
23
|
+
numAddresses: int,
|
|
24
|
+
}
|
|
25
|
+
@tag("status")
|
|
26
|
+
type state =
|
|
27
|
+
| @as("disabled") Disabled({})
|
|
28
|
+
| @as("initializing") Initializing({})
|
|
29
|
+
| @as("active")
|
|
30
|
+
Active({
|
|
31
|
+
envioVersion: string,
|
|
32
|
+
chains: array<chainData>,
|
|
33
|
+
indexerStartTime: Date.t,
|
|
34
|
+
isPreRegisteringDynamicContracts: bool,
|
|
35
|
+
rollbackOnReorg: bool,
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
let toChainData = (m: Metrics.chainMetrics): chainData => {
|
|
39
|
+
chainId: m.chainId,
|
|
40
|
+
poweredByHyperSync: m.poweredByHyperSync,
|
|
41
|
+
firstEventBlockNumber: m.firstEventBlockNumber,
|
|
42
|
+
latestProcessedBlock: m.latestProcessedBlock,
|
|
43
|
+
timestampCaughtUpToHeadOrEndblock: m.timestampCaughtUpToHeadOrEndblock,
|
|
44
|
+
numEventsProcessed: m.numEventsProcessed,
|
|
45
|
+
latestFetchedBlockNumber: m.latestFetchedBlockNumber,
|
|
46
|
+
knownHeight: m.knownHeight,
|
|
47
|
+
numBatchesFetched: m.numBatchesFetched,
|
|
48
|
+
startBlock: m.startBlock,
|
|
49
|
+
endBlock: m.endBlock,
|
|
50
|
+
numAddresses: m.numAddresses,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let chainDataSchema = S.schema((s): chainData => {
|
|
54
|
+
chainId: s.matches(ChainId.schema),
|
|
55
|
+
poweredByHyperSync: s.matches(S.bool),
|
|
56
|
+
firstEventBlockNumber: s.matches(S.option(S.int)),
|
|
57
|
+
latestProcessedBlock: s.matches(S.option(S.int)),
|
|
58
|
+
timestampCaughtUpToHeadOrEndblock: s.matches(S.option(S.datetime(S.string))),
|
|
59
|
+
numEventsProcessed: s.matches(S.float),
|
|
60
|
+
latestFetchedBlockNumber: s.matches(S.int),
|
|
61
|
+
knownHeight: s.matches(S.int),
|
|
62
|
+
numBatchesFetched: s.matches(S.int),
|
|
63
|
+
startBlock: s.matches(S.int),
|
|
64
|
+
endBlock: s.matches(S.option(S.int)),
|
|
65
|
+
numAddresses: s.matches(S.int),
|
|
66
|
+
})
|
|
67
|
+
let stateSchema = S.union([
|
|
68
|
+
S.literal(Disabled({})),
|
|
69
|
+
S.literal(Initializing({})),
|
|
70
|
+
S.schema(s => Active({
|
|
71
|
+
envioVersion: s.matches(S.string),
|
|
72
|
+
chains: s.matches(S.array(chainDataSchema)),
|
|
73
|
+
indexerStartTime: s.matches(S.datetime(S.string)),
|
|
74
|
+
// Keep the field, since Dev Console expects it to be present
|
|
75
|
+
isPreRegisteringDynamicContracts: false,
|
|
76
|
+
rollbackOnReorg: s.matches(S.bool),
|
|
77
|
+
})),
|
|
78
|
+
])
|
|
79
|
+
|
|
80
|
+
// Runtime state lives in the process-wide `EnvioGlobal` record (shared
|
|
81
|
+
// across duplicate envio module instances); the slots are opaque there, so
|
|
82
|
+
// cast them to the real types here.
|
|
83
|
+
let startServer = (
|
|
84
|
+
~getMetrics: unit => option<Metrics.t>,
|
|
85
|
+
~envioVersion: string,
|
|
86
|
+
~onSyncCache: unit => promise<unit>,
|
|
87
|
+
~collectRuntime: unit => string,
|
|
88
|
+
~isDevelopmentMode: bool,
|
|
89
|
+
) => {
|
|
90
|
+
open Express
|
|
91
|
+
|
|
92
|
+
let app = make()
|
|
93
|
+
|
|
94
|
+
let consoleCorsMiddleware = (req, res, next) => {
|
|
95
|
+
switch req.headers->Dict.get("origin") {
|
|
96
|
+
| Some(origin) if origin === Env.prodEnvioAppUrl || origin === Env.envioAppUrl =>
|
|
97
|
+
res->setHeader("Access-Control-Allow-Origin", origin)
|
|
98
|
+
| _ => ()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
res->setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
|
102
|
+
res->setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept")
|
|
103
|
+
|
|
104
|
+
if req.method === Rest.Options {
|
|
105
|
+
res->sendStatus(200)
|
|
106
|
+
} else {
|
|
107
|
+
next()
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
app->useFor("/console", consoleCorsMiddleware)
|
|
111
|
+
app->useFor("/metrics", consoleCorsMiddleware)
|
|
112
|
+
app->useFor("/metrics/runtime", consoleCorsMiddleware)
|
|
113
|
+
|
|
114
|
+
app->get("/healthz", (_req, res) => {
|
|
115
|
+
// this is the machine readable port used in kubernetes to check the health of this service.
|
|
116
|
+
// aditional health information could be added in the future (info about errors, back-offs, etc).
|
|
117
|
+
res->sendStatus(200)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
app->get("/console/state", (_req, res) => {
|
|
121
|
+
let state = if !isDevelopmentMode {
|
|
122
|
+
Disabled({})
|
|
123
|
+
} else {
|
|
124
|
+
switch getMetrics() {
|
|
125
|
+
| None => Initializing({})
|
|
126
|
+
| Some(metrics) =>
|
|
127
|
+
Active({
|
|
128
|
+
envioVersion,
|
|
129
|
+
chains: metrics.chains->Array.map(toChainData),
|
|
130
|
+
indexerStartTime: metrics.startTime,
|
|
131
|
+
isPreRegisteringDynamicContracts: false,
|
|
132
|
+
rollbackOnReorg: metrics.rollbackEnabled,
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
res->json(state->S.reverseConvertToJsonOrThrow(stateSchema))
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
app->post("/console/syncCache", (_req, res) => {
|
|
141
|
+
if isDevelopmentMode {
|
|
142
|
+
onSyncCache()
|
|
143
|
+
->Promise.thenResolve(() => res->json(Boolean(true)))
|
|
144
|
+
// A dump that couldn't be made, or couldn't be confirmed, answers the
|
|
145
|
+
// same `false` a disabled console does. Leaving it unanswered would hold
|
|
146
|
+
// the request open for as long as the indexer runs.
|
|
147
|
+
->Promise.catch(exn => {
|
|
148
|
+
Logging.errorWithExn(exn, "Failed to sync the effect cache")
|
|
149
|
+
res->json(Boolean(false))
|
|
150
|
+
Promise.resolve()
|
|
151
|
+
})
|
|
152
|
+
->Promise.ignore
|
|
153
|
+
} else {
|
|
154
|
+
res->json(Boolean(false))
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
app->get("/metrics", (_req, res) => {
|
|
159
|
+
res->set("Content-Type", Metrics.contentType)
|
|
160
|
+
let _ = res->endWithData(Metrics.collect(~metrics=getMetrics()))
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
app->get("/metrics/runtime", (_req, res) => {
|
|
164
|
+
res->set("Content-Type", Metrics.contentType)
|
|
165
|
+
let _ = res->endWithData(collectRuntime())
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
let server = app->listen(Env.serverPort)
|
|
169
|
+
server->Express.onError(err => {
|
|
170
|
+
let code = (err->(Utils.magic: JsExn.t => {..}))["code"]
|
|
171
|
+
if code === "EADDRINUSE" {
|
|
172
|
+
Logging.error(
|
|
173
|
+
`Port ${Env.serverPort->Int.toString} is already in use. To fix this either:` ++
|
|
174
|
+
`\n 1. Kill the process using the port: lsof -ti :${Env.serverPort->Int.toString} | xargs kill -9` ++ `\n 2. Use a different port by setting the ENVIO_INDEXER_PORT environment variable: ENVIO_INDEXER_PORT=9899 envio start`,
|
|
175
|
+
)
|
|
176
|
+
} else {
|
|
177
|
+
Logging.errorWithExn(err, "Failed to start indexer server")
|
|
178
|
+
}
|
|
179
|
+
NodeJs.process->NodeJs.exitWithCode(Failure)
|
|
180
|
+
})
|
|
181
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Env from "./Env.res.mjs";
|
|
4
|
+
import * as ChainId from "./ChainId.res.mjs";
|
|
5
|
+
import * as Logging from "./Logging.res.mjs";
|
|
6
|
+
import * as Metrics from "./Metrics.res.mjs";
|
|
7
|
+
import Express from "express";
|
|
8
|
+
import * as Process from "process";
|
|
9
|
+
import * as Stdlib_Promise from "@rescript/runtime/lib/es6/Stdlib_Promise.js";
|
|
10
|
+
import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs";
|
|
11
|
+
|
|
12
|
+
function toChainData(m) {
|
|
13
|
+
return {
|
|
14
|
+
chainId: m.chainId,
|
|
15
|
+
poweredByHyperSync: m.poweredByHyperSync,
|
|
16
|
+
firstEventBlockNumber: m.firstEventBlockNumber,
|
|
17
|
+
latestProcessedBlock: m.latestProcessedBlock,
|
|
18
|
+
timestampCaughtUpToHeadOrEndblock: m.timestampCaughtUpToHeadOrEndblock,
|
|
19
|
+
numEventsProcessed: m.numEventsProcessed,
|
|
20
|
+
latestFetchedBlockNumber: m.latestFetchedBlockNumber,
|
|
21
|
+
currentBlockHeight: m.knownHeight,
|
|
22
|
+
numBatchesFetched: m.numBatchesFetched,
|
|
23
|
+
startBlock: m.startBlock,
|
|
24
|
+
endBlock: m.endBlock,
|
|
25
|
+
numAddresses: m.numAddresses
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let chainDataSchema = S$RescriptSchema.schema(s => ({
|
|
30
|
+
chainId: s.m(ChainId.schema),
|
|
31
|
+
poweredByHyperSync: s.m(S$RescriptSchema.bool),
|
|
32
|
+
firstEventBlockNumber: s.m(S$RescriptSchema.option(S$RescriptSchema.int)),
|
|
33
|
+
latestProcessedBlock: s.m(S$RescriptSchema.option(S$RescriptSchema.int)),
|
|
34
|
+
timestampCaughtUpToHeadOrEndblock: s.m(S$RescriptSchema.option(S$RescriptSchema.datetime(S$RescriptSchema.string, undefined))),
|
|
35
|
+
numEventsProcessed: s.m(S$RescriptSchema.float),
|
|
36
|
+
latestFetchedBlockNumber: s.m(S$RescriptSchema.int),
|
|
37
|
+
currentBlockHeight: s.m(S$RescriptSchema.int),
|
|
38
|
+
numBatchesFetched: s.m(S$RescriptSchema.int),
|
|
39
|
+
startBlock: s.m(S$RescriptSchema.int),
|
|
40
|
+
endBlock: s.m(S$RescriptSchema.option(S$RescriptSchema.int)),
|
|
41
|
+
numAddresses: s.m(S$RescriptSchema.int)
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
let stateSchema = S$RescriptSchema.union([
|
|
45
|
+
S$RescriptSchema.literal({
|
|
46
|
+
status: "disabled"
|
|
47
|
+
}),
|
|
48
|
+
S$RescriptSchema.literal({
|
|
49
|
+
status: "initializing"
|
|
50
|
+
}),
|
|
51
|
+
S$RescriptSchema.schema(s => ({
|
|
52
|
+
status: "active",
|
|
53
|
+
envioVersion: s.m(S$RescriptSchema.string),
|
|
54
|
+
chains: s.m(S$RescriptSchema.array(chainDataSchema)),
|
|
55
|
+
indexerStartTime: s.m(S$RescriptSchema.datetime(S$RescriptSchema.string, undefined)),
|
|
56
|
+
isPreRegisteringDynamicContracts: false,
|
|
57
|
+
rollbackOnReorg: s.m(S$RescriptSchema.bool)
|
|
58
|
+
}))
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
function startServer(getMetrics, envioVersion, onSyncCache, collectRuntime, isDevelopmentMode) {
|
|
62
|
+
let app = Express();
|
|
63
|
+
let consoleCorsMiddleware = (req, res, next) => {
|
|
64
|
+
let origin = req.headers["origin"];
|
|
65
|
+
if (origin !== undefined && (origin === Env.prodEnvioAppUrl || origin === Env.envioAppUrl)) {
|
|
66
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
67
|
+
}
|
|
68
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
|
69
|
+
res.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
|
|
70
|
+
if (req.method === "OPTIONS") {
|
|
71
|
+
res.sendStatus(200);
|
|
72
|
+
return;
|
|
73
|
+
} else {
|
|
74
|
+
return next();
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
app.use("/console", consoleCorsMiddleware);
|
|
78
|
+
app.use("/metrics", consoleCorsMiddleware);
|
|
79
|
+
app.use("/metrics/runtime", consoleCorsMiddleware);
|
|
80
|
+
app.get("/healthz", (_req, res) => {
|
|
81
|
+
res.sendStatus(200);
|
|
82
|
+
});
|
|
83
|
+
app.get("/console/state", (_req, res) => {
|
|
84
|
+
let state;
|
|
85
|
+
if (isDevelopmentMode) {
|
|
86
|
+
let metrics = getMetrics();
|
|
87
|
+
state = metrics !== undefined ? ({
|
|
88
|
+
status: "active",
|
|
89
|
+
envioVersion: envioVersion,
|
|
90
|
+
chains: metrics.chains.map(toChainData),
|
|
91
|
+
indexerStartTime: metrics.startTime,
|
|
92
|
+
isPreRegisteringDynamicContracts: false,
|
|
93
|
+
rollbackOnReorg: metrics.rollbackEnabled
|
|
94
|
+
}) : ({
|
|
95
|
+
status: "initializing"
|
|
96
|
+
});
|
|
97
|
+
} else {
|
|
98
|
+
state = {
|
|
99
|
+
status: "disabled"
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
res.json(S$RescriptSchema.reverseConvertToJsonOrThrow(state, stateSchema));
|
|
103
|
+
});
|
|
104
|
+
app.post("/console/syncCache", (_req, res) => {
|
|
105
|
+
if (isDevelopmentMode) {
|
|
106
|
+
Stdlib_Promise.$$catch(onSyncCache().then(() => {
|
|
107
|
+
res.json(true);
|
|
108
|
+
}), exn => {
|
|
109
|
+
Logging.errorWithExn(exn, "Failed to sync the effect cache");
|
|
110
|
+
res.json(false);
|
|
111
|
+
return Promise.resolve();
|
|
112
|
+
});
|
|
113
|
+
} else {
|
|
114
|
+
res.json(false);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
app.get("/metrics", (_req, res) => {
|
|
118
|
+
res.set("Content-Type", Metrics.contentType);
|
|
119
|
+
res.end(Metrics.collect(getMetrics()));
|
|
120
|
+
});
|
|
121
|
+
app.get("/metrics/runtime", (_req, res) => {
|
|
122
|
+
res.set("Content-Type", Metrics.contentType);
|
|
123
|
+
res.end(collectRuntime());
|
|
124
|
+
});
|
|
125
|
+
let server = app.listen(Env.serverPort);
|
|
126
|
+
server.on("error", err => {
|
|
127
|
+
let code = err.code;
|
|
128
|
+
if (code === "EADDRINUSE") {
|
|
129
|
+
Logging.error(`Port ` + Env.serverPort.toString() + ` is already in use. To fix this either:` + (`\n 1. Kill the process using the port: lsof -ti :` + Env.serverPort.toString() + ` | xargs kill -9`) + `\n 2. Use a different port by setting the ENVIO_INDEXER_PORT environment variable: ENVIO_INDEXER_PORT=9899 envio start`);
|
|
130
|
+
} else {
|
|
131
|
+
Logging.errorWithExn(err, "Failed to start indexer server");
|
|
132
|
+
}
|
|
133
|
+
Process.exit(1);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export {
|
|
138
|
+
toChainData,
|
|
139
|
+
chainDataSchema,
|
|
140
|
+
stateSchema,
|
|
141
|
+
startServer,
|
|
142
|
+
}
|
|
143
|
+
/* chainDataSchema Not a pure module */
|