fix(health): fix issue where chain id wasn't cached#98
Conversation
|
I'm not sure if this entirely correct, because now the health check doesn't actually verify that the proxy can reach the chain in future health checks. Maybe wrap it in a cache with TTL until we have time to properly think of how to do a health check mechanism that harmonises core and Fast? |
…t using the chain
aa88d20 to
48cdfbb
Compare
|
@Thomasvdam I changed it to do without caching because we do indeed want to know which proxies are unhealthy. Instead i've opted for just the fastOnly flag. Then the health endpoint won't return a unhealthy status when the chain is upgrading. |
|
Also made the better effect version, this is just a hotfix |
| import { Duration, Match, Option, Runtime } from "effect"; | ||
| import { Effect } from "effect"; |
There was a problem hiding this comment.
| import { Duration, Match, Option, Runtime } from "effect"; | |
| import { Effect } from "effect"; | |
| import { Duration, Effect, Match, Option, Runtime } from "effect"; |
| }); | ||
| }); | ||
| if (config.fastOnly) { | ||
| healthy = true; |
| }); | ||
| group.get("info", async () => { | ||
| const chainId = config.fastOnly | ||
| ? Result.ok(dataProxy.options.chainId) |
There was a problem hiding this comment.
should this not be null?
Just to avoid that if it is fastOnly, it is clear that chainId was not being used (even though it might be set).
| export const statusPlugin = ( | ||
| context: Context, | ||
| dataProxy: DataProxy, | ||
| options: Config["statusEndpoints"], | ||
| config: Config, | ||
| ) => | ||
| Effect.gen(function* () { | ||
| const runtime = yield* Effect.runtime(); | ||
|
|
||
| return (app: Elysia) => { | ||
| const plugin = new Elysia({ | ||
| name: "status", | ||
| }); | ||
| return (app: Elysia) => | ||
| Runtime.runSync( | ||
| runtime, | ||
| Effect.gen(function* () { | ||
| const plugin = new Elysia({ | ||
| name: "status", | ||
| }); | ||
|
|
||
| plugin.group(options.root, (group) => { | ||
| if (options.apiKey) { | ||
| const { header, secret } = options.apiKey; | ||
| group.onBeforeHandle(({ request }) => { | ||
| const apiKey = request.headers.get(header); | ||
| if (apiKey !== secret) { | ||
| return new Response("Unauthorized", { status: 401 }); | ||
| plugin.group(options.root, (group) => { | ||
| if (options.apiKey) { | ||
| const { header, secret } = options.apiKey; | ||
| group.onBeforeHandle(({ request }) => { | ||
| const apiKey = request.headers.get(header); | ||
| if (apiKey !== secret) { | ||
| return new Response("Unauthorized", { status: 401 }); | ||
| } | ||
| }); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| // List all available endpoints | ||
| group.get("", () => { | ||
| return Response.json({ | ||
| endpoints: [`${options.root}/health`, `${options.root}/info`], | ||
| }); | ||
| }); | ||
| // List all available endpoints | ||
| group.get("", () => { | ||
| return Response.json({ | ||
| endpoints: [`${options.root}/health`, `${options.root}/info`], | ||
| }); | ||
| }); | ||
|
|
||
| group.get("health", async ({ set }) => { | ||
| const chainId = await effectToAsyncResult( | ||
| getRpcChainId(dataProxy.options.rpcUrl), | ||
| ); | ||
| const hasCorrectChainId = | ||
| chainId.isOk && chainId.value === dataProxy.options.chainId; | ||
| set.status = chainId.isOk && hasCorrectChainId ? 200 : 500; | ||
| group.get("health", async ({ set }) => { | ||
| let healthy = true; | ||
|
|
||
| return Response.json({ | ||
| status: chainId.isOk && hasCorrectChainId ? "healthy" : "unhealthy", | ||
| metrics: context.getMetrics(), | ||
| }); | ||
| }); | ||
| if (config.fastOnly) { | ||
| healthy = true; | ||
| set.status = 200; | ||
| } else { | ||
| const chainId = await effectToAsyncResult(runtime, getRpcChainId(dataProxy.options.rpcUrl)); | ||
| const hasCorrectChainId = | ||
| chainId.isOk && chainId.value === dataProxy.options.chainId; | ||
|
|
||
| group.get("info", async () => { | ||
| const chainId = await effectToAsyncResult( | ||
| getRpcChainId(dataProxy.options.rpcUrl), | ||
| ); | ||
| set.status = chainId.isOk && hasCorrectChainId ? 200 : 500; | ||
| healthy = chainId.isOk && hasCorrectChainId; | ||
| } | ||
|
|
||
| return Response.json({ | ||
| pubKey: context.getPublicKey(), | ||
| fastConfig: context.getFastConfig(), | ||
| version: getVersions().proxy, | ||
| chainId: dataProxy.options.chainId, | ||
| rpcChainId: chainId.isOk ? chainId.value : null, | ||
| }); | ||
| }); | ||
| return Response.json({ | ||
| status: healthy ? "healthy" : "unhealthy", | ||
| metrics: context.getMetrics(), | ||
| }); | ||
| }); | ||
|
|
||
| return group; | ||
| }); | ||
| group.get("info", async () => { | ||
| const chainId = config.fastOnly | ||
| ? Result.ok(dataProxy.options.chainId) | ||
| : await effectToAsyncResult(runtime, getRpcChainId(dataProxy.options.rpcUrl)); | ||
|
|
||
| Runtime.runSync( | ||
| runtime, | ||
| Effect.logInfo( | ||
| `Status endpoints: /${options.root}/health, /${options.root}/info`, | ||
| ), | ||
| ); | ||
| return Response.json({ | ||
| pubKey: context.getPublicKey(), | ||
| fastConfig: context.getFastConfig(), | ||
| version: getVersions().proxy, | ||
| chainId: dataProxy.options.chainId, | ||
| rpcChainId: chainId.isOk ? chainId.value : null, | ||
| }); | ||
| }); | ||
|
|
||
| return app.use(plugin); | ||
| }; | ||
| return group; | ||
| }); | ||
|
|
||
| logger.info( | ||
| `Status endpoints: /${options.root}/health, /${options.root}/info`, | ||
| ); | ||
| return app.use(plugin); | ||
| }), | ||
| ); | ||
| }); |
There was a problem hiding this comment.
I think that if we add the runtime as an expected input param, we could simplify a little bit the statusPlugin code.
On the proxy-server.ts:
// Get the runtime once at startup
const runtime = Effect.runSync(Effect.runtime<never>());
server.use(
statusPlugin(statusContext, dataProxy, config.statusEndpoints, config, runtime),
);
Entirely optional, we could avoid the outer Effect.gen:
export const statusPlugin = (
context: Context,
dataProxy: DataProxy,
options: Config["statusEndpoints"],
config: Config,
runtime: Runtime.Runtime<never>, // ← accept runtime as param
) =>
(app: Elysia) => {
const plugin = new Elysia({ name: "status" });
plugin.group(options.root, (group) => {
// ... all the route handlers, using runtime directly
});
logger.info(`Status endpoints: /${options.root}/health, /${options.root}/info`);
return app.use(plugin);
};
mariocao
left a comment
There was a problem hiding this comment.
As I was told, you can forget about my comment regarding the Effect.gen as this is more a PR for a fix. We can improve that in another PR later.
Motivation
(Write your motivation here)
Explanation of Changes
(Write your explanation here)
Testing
(Write your test plan here)
Related PRs and Issues
(Link your related PRs and Issues here)