feat: add graceful shutdown and better deployment environment logic

This commit is contained in:
Ritesh Ghosh
2025-05-25 22:48:39 +05:30
parent 1017371432
commit b19078e29c
+72 -22
View File
@@ -4,12 +4,13 @@ import { Hono } from "hono";
import { serve } from "@hono/node-server"; import { serve } from "@hono/node-server";
import { serveStatic } from "@hono/node-server/serve-static"; import { serveStatic } from "@hono/node-server/serve-static";
import { env } from "./config/env.js";
import { log } from "./config/logger.js"; import { log } from "./config/logger.js";
import { corsConfig } from "./config/cors.js"; import { corsConfig } from "./config/cors.js";
import { ratelimit } from "./config/ratelimit.js"; import { ratelimit } from "./config/ratelimit.js";
import { execGracefulShutdown } from "./utils.js";
import { DeploymentEnv, env, SERVERLESS_ENVIRONMENTS } from "./config/env.js";
import { errorHandler, notFoundHandler } from "./config/errorHandler.js"; import { errorHandler, notFoundHandler } from "./config/errorHandler.js";
import type { AniwatchAPIVariables } from "./config/variables.js"; import type { ServerContext } from "./config/variables.js";
import { hianimeRouter } from "./routes/hianime.js"; import { hianimeRouter } from "./routes/hianime.js";
import { logging } from "./middleware/logging.js"; import { logging } from "./middleware/logging.js";
@@ -20,22 +21,30 @@ import pkgJson from "../package.json" with { type: "json" };
// //
const BASE_PATH = "/api/v2" as const; const BASE_PATH = "/api/v2" as const;
const app = new Hono<{ Variables: AniwatchAPIVariables }>(); const app = new Hono<ServerContext>();
app.use(logging); app.use(logging);
app.use(corsConfig); app.use(corsConfig);
app.use(cacheControl); app.use(cacheControl);
// /*
// CAUTION: For personal deployments, "refrain" from having an env CAUTION:
// named "ANIWATCH_API_HOSTNAME". You may face rate limitting Having the "ANIWATCH_API_HOSTNAME" env will
// or other issues if you do. enable rate limitting for the deployment.
const ISNT_PERSONAL_DEPLOYMENT = Boolean(env.ANIWATCH_API_HOSTNAME); WARNING:
if (ISNT_PERSONAL_DEPLOYMENT) { If you are using any serverless environment, you must set the
"ANIWATCH_API_DEPLOYMENT_ENV" to that environment's name,
otherwise you may face issues.
*/
const isPersonalDeployment = Boolean(env.ANIWATCH_API_HOSTNAME);
if (isPersonalDeployment) {
app.use(ratelimit); app.use(ratelimit);
} }
app.use("/", serveStatic({ root: "public" })); if (env.ANIWATCH_API_DEPLOYMENT_ENV === DeploymentEnv.NODEJS) {
app.use("/", serveStatic({ root: "public" }));
}
app.get("/health", (c) => c.text("daijoubu", { status: 200 })); app.get("/health", (c) => c.text("daijoubu", { status: 200 }));
app.get("/v", async (c) => app.get("/v", async (c) =>
c.text( c.text(
@@ -55,31 +64,72 @@ app.notFound(notFoundHandler);
app.onError(errorHandler); app.onError(errorHandler);
// //
// NOTE: this env is "required" for vercel deployments (function () {
if (!env.ANIWATCH_API_VERCEL_DEPLOYMENT) { /*
serve({ NOTE:
"ANIWATCH_API_DEPLOYMENT_MODE" env must be set to
its supported name for serverless deployments
Eg: "vercel" for vercel deployments
*/
if (SERVERLESS_ENVIRONMENTS.includes(env.ANIWATCH_API_DEPLOYMENT_ENV)) {
return;
}
const server = serve({
port: env.ANIWATCH_API_PORT, port: env.ANIWATCH_API_PORT,
fetch: app.fetch, fetch: app.fetch,
}).addListener("listening", () => { }).addListener("listening", () =>
log.info( log.info(
`aniwatch-api RUNNING at http://localhost:${env.ANIWATCH_API_PORT}` `aniwatch-api RUNNING at http://localhost:${env.ANIWATCH_API_PORT}`
)
); );
process.on("SIGINT", () => execGracefulShutdown(server));
process.on("SIGTERM", () => execGracefulShutdown(server));
process.on("uncaughtException", (err) => {
log.error(`Uncaught Exception: ${err.message}`);
execGracefulShutdown(server);
});
process.on("unhandledRejection", (reason, promise) => {
log.error(
`Unhandled Rejection at: ${promise}, reason: ${reason instanceof Error ? reason.message : reason}`
);
execGracefulShutdown(server);
}); });
// NOTE: remove the `if` block below for personal deployments /*
if (ISNT_PERSONAL_DEPLOYMENT) { CAUTION:
const interval = 9 * 60 * 1000; // 9mins The `if` below block is for `render free deployments` only,
as their free tier has an approx 10 or 15 minute sleep time.
This is to keep the server awake and prevent it from sleeping.
You can enable the automatic health check by setting the
environment variables "ANIWATCH_API_HOSTNAME" to your deployment's hostname,
and "ANIWATCH_API_DEPLOYMENT_ENV" to "render" in your environment variables.
If you are not using render, you can remove the below `if` block.
*/
if (
isPersonalDeployment &&
env.ANIWATCH_API_DEPLOYMENT_ENV === DeploymentEnv.RENDER
) {
const INTERVAL_DELAY = 8 * 60 * 1000; // 8mins
const url = new URL(`https://${env.ANIWATCH_API_HOSTNAME}/health`);
// don't sleep // don't sleep
setInterval(() => { setInterval(() => {
https
.get(url.href)
.on("response", () => {
log.info( log.info(
`aniwatch-api HEALTH_CHECK at ${new Date().toISOString()}` `aniwatch-api HEALTH_CHECK at ${new Date().toISOString()}`
); );
https })
.get(`https://${env.ANIWATCH_API_HOSTNAME}/health`) .on("error", (err) =>
.on("error", (err) => log.error(err.message.trim())); log.warn(
}, interval); `aniwatch-api HEALTH_CHECK failed; ${err.message.trim()}`
)
);
}, INTERVAL_DELAY);
} }
} })();
export default app; export default app;