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
+74 -24
View File
@@ -4,12 +4,13 @@ import { Hono } from "hono";
import { serve } from "@hono/node-server";
import { serveStatic } from "@hono/node-server/serve-static";
import { env } from "./config/env.js";
import { log } from "./config/logger.js";
import { corsConfig } from "./config/cors.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 type { AniwatchAPIVariables } from "./config/variables.js";
import type { ServerContext } from "./config/variables.js";
import { hianimeRouter } from "./routes/hianime.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 app = new Hono<{ Variables: AniwatchAPIVariables }>();
const app = new Hono<ServerContext>();
app.use(logging);
app.use(corsConfig);
app.use(cacheControl);
//
// CAUTION: For personal deployments, "refrain" from having an env
// named "ANIWATCH_API_HOSTNAME". You may face rate limitting
// or other issues if you do.
const ISNT_PERSONAL_DEPLOYMENT = Boolean(env.ANIWATCH_API_HOSTNAME);
if (ISNT_PERSONAL_DEPLOYMENT) {
/*
CAUTION:
Having the "ANIWATCH_API_HOSTNAME" env will
enable rate limitting for the deployment.
WARNING:
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("/", 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("/v", async (c) =>
c.text(
@@ -55,31 +64,72 @@ app.notFound(notFoundHandler);
app.onError(errorHandler);
//
// NOTE: this env is "required" for vercel deployments
if (!env.ANIWATCH_API_VERCEL_DEPLOYMENT) {
serve({
(function () {
/*
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,
fetch: app.fetch,
}).addListener("listening", () => {
}).addListener("listening", () =>
log.info(
`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) {
const interval = 9 * 60 * 1000; // 9mins
/*
CAUTION:
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
setInterval(() => {
log.info(
`aniwatch-api HEALTH_CHECK at ${new Date().toISOString()}`
);
https
.get(`https://${env.ANIWATCH_API_HOSTNAME}/health`)
.on("error", (err) => log.error(err.message.trim()));
}, interval);
.get(url.href)
.on("response", () => {
log.info(
`aniwatch-api HEALTH_CHECK at ${new Date().toISOString()}`
);
})
.on("error", (err) =>
log.warn(
`aniwatch-api HEALTH_CHECK failed; ${err.message.trim()}`
)
);
}, INTERVAL_DELAY);
}
}
})();
export default app;