fix: เพิ่ม Graceful Shutdown - ป้องกัน connection in app file, Log Mnddleware + Memory Store
This commit is contained in:
parent
a194d8594b
commit
e068aafe3a
3 changed files with 137 additions and 33 deletions
46
src/app.ts
46
src/app.ts
|
|
@ -11,6 +11,7 @@ import { AppDataSource } from "./database/data-source";
|
||||||
import { RegisterRoutes } from "./routes";
|
import { RegisterRoutes } from "./routes";
|
||||||
import { OrganizationController } from "./controllers/OrganizationController";
|
import { OrganizationController } from "./controllers/OrganizationController";
|
||||||
import logMiddleware from "./middlewares/logs";
|
import logMiddleware from "./middlewares/logs";
|
||||||
|
import { logMemoryStore } from "./utils/log-memory-store";
|
||||||
import { CommandController } from "./controllers/CommandController";
|
import { CommandController } from "./controllers/CommandController";
|
||||||
import { ProfileSalaryController } from "./controllers/ProfileSalaryController";
|
import { ProfileSalaryController } from "./controllers/ProfileSalaryController";
|
||||||
import { DateSerializer } from "./interfaces/date-serializer";
|
import { DateSerializer } from "./interfaces/date-serializer";
|
||||||
|
|
@ -20,6 +21,9 @@ import { initWebSocket } from "./services/webSocket";
|
||||||
async function main() {
|
async function main() {
|
||||||
await AppDataSource.initialize();
|
await AppDataSource.initialize();
|
||||||
|
|
||||||
|
// Initialize LogMemoryStore after database is ready
|
||||||
|
logMemoryStore.initialize();
|
||||||
|
|
||||||
// Setup custom Date serialization for local timezone
|
// Setup custom Date serialization for local timezone
|
||||||
DateSerializer.setupDateSerialization();
|
DateSerializer.setupDateSerialization();
|
||||||
|
|
||||||
|
|
@ -93,7 +97,7 @@ async function main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
// app.listen(APP_PORT, APP_HOST, () => console.log(`Listening on: http://localhost:${APP_PORT}`));
|
// app.listen(APP_PORT, APP_HOST, () => console.log(`Listening on: http://localhost:${APP_PORT}`));
|
||||||
app.listen(
|
const server = app.listen(
|
||||||
APP_PORT,
|
APP_PORT,
|
||||||
APP_HOST,
|
APP_HOST,
|
||||||
() => (
|
() => (
|
||||||
|
|
@ -111,6 +115,46 @@ async function main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
runMessageQueue();
|
runMessageQueue();
|
||||||
|
|
||||||
|
// Graceful Shutdown
|
||||||
|
const gracefulShutdown = async (signal: string) => {
|
||||||
|
console.log(`\n[APP] ${signal} received. Starting graceful shutdown...`);
|
||||||
|
|
||||||
|
// Stop accepting new connections
|
||||||
|
server.close(() => {
|
||||||
|
console.log("[APP] HTTP server closed");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Force close after timeout
|
||||||
|
const shutdownTimeout = setTimeout(() => {
|
||||||
|
console.error("[APP] Forced shutdown after timeout");
|
||||||
|
process.exit(1);
|
||||||
|
}, 30000); // 30 seconds timeout
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Close database connections
|
||||||
|
if (AppDataSource.isInitialized) {
|
||||||
|
await AppDataSource.destroy();
|
||||||
|
console.log("[APP] Database connections closed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy LogMemoryStore
|
||||||
|
logMemoryStore.destroy();
|
||||||
|
console.log("[APP] LogMemoryStore destroyed");
|
||||||
|
|
||||||
|
clearTimeout(shutdownTimeout);
|
||||||
|
console.log("[APP] Graceful shutdown completed");
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[APP] Error during shutdown:", error);
|
||||||
|
clearTimeout(shutdownTimeout);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Listen for shutdown signals
|
||||||
|
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
|
||||||
|
process.on("SIGINT", () => gracefulShutdown("SIGINT"));
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main();
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
import { NextFunction, Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
import { Client } from "@elastic/elasticsearch";
|
import { Client } from "@elastic/elasticsearch";
|
||||||
import { AppDataSource } from "../database/data-source";
|
import { logMemoryStore } from "../utils/log-memory-store";
|
||||||
import { PosMaster } from "../entities/PosMaster";
|
|
||||||
import { OrgRevision } from "../entities/OrgRevision";
|
|
||||||
import { Profile } from "../entities/Profile";
|
|
||||||
|
|
||||||
if (!process.env.ELASTICSEARCH_INDEX) {
|
if (!process.env.ELASTICSEARCH_INDEX) {
|
||||||
throw new Error("Require ELASTICSEARCH_INDEX to store log.");
|
throw new Error("Require ELASTICSEARCH_INDEX to store log.");
|
||||||
|
|
@ -27,9 +24,6 @@ async function logMiddleware(req: Request, res: Response, next: NextFunction) {
|
||||||
if (!req.url.startsWith("/api/")) return next();
|
if (!req.url.startsWith("/api/")) return next();
|
||||||
|
|
||||||
let data: any;
|
let data: any;
|
||||||
const repoPosmaster = AppDataSource.getRepository(PosMaster);
|
|
||||||
const repoProfile = AppDataSource.getRepository(Profile);
|
|
||||||
const repoRevision = AppDataSource.getRepository(OrgRevision);
|
|
||||||
|
|
||||||
const originalJson = res.json;
|
const originalJson = res.json;
|
||||||
|
|
||||||
|
|
@ -42,13 +36,6 @@ async function logMiddleware(req: Request, res: Response, next: NextFunction) {
|
||||||
|
|
||||||
req.app.locals.logData = {};
|
req.app.locals.logData = {};
|
||||||
|
|
||||||
const revision = await repoRevision.findOne({
|
|
||||||
where: {
|
|
||||||
orgRevisionIsCurrent: true,
|
|
||||||
orgRevisionIsDraft: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
res.on("finish", async () => {
|
res.on("finish", async () => {
|
||||||
try {
|
try {
|
||||||
if (!req.url.startsWith("/api/")) return;
|
if (!req.url.startsWith("/api/")) return;
|
||||||
|
|
@ -79,16 +66,15 @@ async function logMiddleware(req: Request, res: Response, next: NextFunction) {
|
||||||
if (req.url.startsWith("/api/v1/org/keycloak/")) system = "registry";
|
if (req.url.startsWith("/api/v1/org/keycloak/")) system = "registry";
|
||||||
|
|
||||||
const level = LOG_LEVEL_MAP[process.env.LOG_LEVEL ?? "debug"] || 4;
|
const level = LOG_LEVEL_MAP[process.env.LOG_LEVEL ?? "debug"] || 4;
|
||||||
const profileByKeycloak = await repoProfile.findOne({
|
|
||||||
where: { keycloak: req.app.locals.logData.userId },
|
// Get profile from cache
|
||||||
});
|
const profileByKeycloak = await logMemoryStore.getProfileByKeycloak(
|
||||||
const rootId = await repoPosmaster.findOne({
|
req.app.locals.logData.userId,
|
||||||
where: {
|
);
|
||||||
current_holderId: profileByKeycloak?.id,
|
|
||||||
orgRevisionId: revision?.id,
|
// Get rootId from cache
|
||||||
},
|
const rootId = await logMemoryStore.getRootIdByProfileId(profileByKeycloak?.id);
|
||||||
select: ["orgRootId"],
|
// console.log("ancestorDNA:", rootId);
|
||||||
});
|
|
||||||
|
|
||||||
if (level === 1 && res.statusCode < 500) return;
|
if (level === 1 && res.statusCode < 500) return;
|
||||||
if (level === 2 && res.statusCode < 400) return;
|
if (level === 2 && res.statusCode < 400) return;
|
||||||
|
|
@ -96,7 +82,7 @@ async function logMiddleware(req: Request, res: Response, next: NextFunction) {
|
||||||
const obj = {
|
const obj = {
|
||||||
logType: res.statusCode >= 500 ? "error" : res.statusCode >= 400 ? "warning" : "info",
|
logType: res.statusCode >= 500 ? "error" : res.statusCode >= 400 ? "warning" : "info",
|
||||||
ip: req.ip,
|
ip: req.ip,
|
||||||
rootId: rootId?.orgRootId ?? null,
|
rootId: rootId ?? null,
|
||||||
systemName: system,
|
systemName: system,
|
||||||
startTimeStamp: timestamp,
|
startTimeStamp: timestamp,
|
||||||
endTimeStamp: new Date().toISOString(),
|
endTimeStamp: new Date().toISOString(),
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,23 @@
|
||||||
import { AppDataSource } from "../database/data-source";
|
import { AppDataSource } from "../database/data-source";
|
||||||
import { OrgRevision } from "../entities/OrgRevision";
|
import { OrgRevision } from "../entities/OrgRevision";
|
||||||
|
import { Profile } from "../entities/Profile";
|
||||||
|
import { PosMaster } from "../entities/PosMaster";
|
||||||
|
|
||||||
interface LogCacheData {
|
interface LogCacheData {
|
||||||
currentRevision: OrgRevision | null;
|
currentRevision: OrgRevision | null;
|
||||||
|
profileCache: Map<string, Profile>; // keycloak → Profile
|
||||||
|
rootIdCache: Map<string, string>; // profileId → rootId
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
class LogMemoryStore {
|
class LogMemoryStore {
|
||||||
private cache: LogCacheData = {
|
private cache: LogCacheData = {
|
||||||
currentRevision: null,
|
currentRevision: null,
|
||||||
|
profileCache: new Map(),
|
||||||
|
rootIdCache: new Map(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
};
|
};
|
||||||
private readonly REFRESH_INTERVAL = 5 * 60 * 1000; // 5 นาที
|
private readonly REFRESH_INTERVAL = 10 * 60 * 1000; // 10 minutes
|
||||||
private isRefreshing = false;
|
private isRefreshing = false;
|
||||||
private isInitialized = false;
|
private isInitialized = false;
|
||||||
private refreshTimer: NodeJS.Timeout | null = null;
|
private refreshTimer: NodeJS.Timeout | null = null;
|
||||||
|
|
@ -33,7 +39,11 @@ class LogMemoryStore {
|
||||||
this.refreshCache();
|
this.refreshCache();
|
||||||
}, this.REFRESH_INTERVAL);
|
}, this.REFRESH_INTERVAL);
|
||||||
|
|
||||||
console.log("[LogMemoryStore] Initialized with", this.REFRESH_INTERVAL / 1000, "second refresh interval");
|
console.log(
|
||||||
|
"[LogMemoryStore] Initialized with",
|
||||||
|
this.REFRESH_INTERVAL / 1000,
|
||||||
|
"second refresh interval",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async refreshCache() {
|
private async refreshCache() {
|
||||||
|
|
@ -44,6 +54,7 @@ class LogMemoryStore {
|
||||||
|
|
||||||
this.isRefreshing = true;
|
this.isRefreshing = true;
|
||||||
try {
|
try {
|
||||||
|
// Refresh revision cache
|
||||||
const repoRevision = AppDataSource.getRepository(OrgRevision);
|
const repoRevision = AppDataSource.getRepository(OrgRevision);
|
||||||
const revision = await repoRevision.findOne({
|
const revision = await repoRevision.findOne({
|
||||||
where: {
|
where: {
|
||||||
|
|
@ -52,11 +63,13 @@ class LogMemoryStore {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.cache.currentRevision = revision;
|
this.cache.currentRevision = revision;
|
||||||
|
|
||||||
|
// Clear on-demand caches (they will be rebuilt as needed)
|
||||||
|
this.cache.profileCache.clear();
|
||||||
|
this.cache.rootIdCache.clear();
|
||||||
|
|
||||||
this.cache.updatedAt = new Date();
|
this.cache.updatedAt = new Date();
|
||||||
console.log(
|
console.log("[LogMemoryStore] Cache refreshed at", this.cache.updatedAt.toISOString());
|
||||||
"[LogMemoryStore] Cache refreshed at",
|
|
||||||
this.cache.updatedAt.toISOString(),
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[LogMemoryStore] Error refreshing cache:", error);
|
console.error("[LogMemoryStore] Error refreshing cache:", error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -72,6 +85,67 @@ class LogMemoryStore {
|
||||||
return this.cache.updatedAt;
|
return this.cache.updatedAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Profile by keycloak ID with caching
|
||||||
|
*/
|
||||||
|
async getProfileByKeycloak(keycloak: string): Promise<Profile | null> {
|
||||||
|
// Check cache first
|
||||||
|
if (this.cache.profileCache.has(keycloak)) {
|
||||||
|
return this.cache.profileCache.get(keycloak)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch from database
|
||||||
|
const repoProfile = AppDataSource.getRepository(Profile);
|
||||||
|
const profile = await repoProfile.findOne({
|
||||||
|
where: { keycloak },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cache the result
|
||||||
|
if (profile) {
|
||||||
|
this.cache.profileCache.set(keycloak, profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get RootId by profileId with caching
|
||||||
|
*/
|
||||||
|
async getRootIdByProfileId(profileId: string | undefined): Promise<string | null> {
|
||||||
|
if (!profileId) return null;
|
||||||
|
|
||||||
|
// Check cache first
|
||||||
|
if (this.cache.rootIdCache.has(profileId)) {
|
||||||
|
return this.cache.rootIdCache.get(profileId)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch from database
|
||||||
|
const repoPosmaster = AppDataSource.getRepository(PosMaster);
|
||||||
|
const revision = this.getCurrentRevision();
|
||||||
|
//
|
||||||
|
const posMaster = await repoPosmaster.findOne({
|
||||||
|
where: {
|
||||||
|
current_holderId: profileId,
|
||||||
|
orgRevisionId: revision?.id,
|
||||||
|
},
|
||||||
|
relations: ["orgRoot"],
|
||||||
|
select: {
|
||||||
|
orgRoot: {
|
||||||
|
ancestorDNA: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const rootId = posMaster?.orgRoot?.ancestorDNA ?? null;
|
||||||
|
|
||||||
|
// Cache the result
|
||||||
|
if (rootId) {
|
||||||
|
this.cache.rootIdCache.set(profileId, rootId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rootId;
|
||||||
|
}
|
||||||
|
|
||||||
// สำหรับ shutdown
|
// สำหรับ shutdown
|
||||||
destroy() {
|
destroy() {
|
||||||
if (this.refreshTimer) {
|
if (this.refreshTimer) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue