From b19078e29cbd86d46d4ef1c5e5c6e914d1c8cc2f Mon Sep 17 00:00:00 2001 From: Ritesh Ghosh Date: Sun, 25 May 2025 22:48:39 +0530 Subject: [PATCH] feat: add graceful shutdown and better deployment environment logic --- src/server.ts | 98 ++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 74 insertions(+), 24 deletions(-) diff --git a/src/server.ts b/src/server.ts index 15bc93e..0cd6d53 100644 --- a/src/server.ts +++ b/src/server.ts @@ -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(); 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;