Changed LogMemoryStore from active refresh (setInterval) to passive refresh on-access (60 min TTL)

This commit is contained in:
Warunee Tamkoo 2026-01-29 00:30:34 +07:00
parent 7955c855bc
commit 656c2e7341
3 changed files with 32 additions and 22 deletions

View file

@ -18,6 +18,7 @@ All notable changes to this project will be documented in this file.
- Extended OrgStructureCache TTL from 10 to 30 minutes (reduce cleanup frequency) - Extended OrgStructureCache TTL from 10 to 30 minutes (reduce cleanup frequency)
- Added OrgStructureCache.destroy() in graceful shutdown handler - Added OrgStructureCache.destroy() in graceful shutdown handler
- Changed LogMemoryStore from active refresh (setInterval) to passive refresh on-access (60 min TTL)
### ⚙️ Miscellaneous Tasks ### ⚙️ Miscellaneous Tasks

View file

@ -104,15 +104,17 @@ class LogMemoryStore {
profileCache: Map<string, Profile>, // keycloak → Profile profileCache: Map<string, Profile>, // keycloak → Profile
rootIdCache: Map<string, string>, // profileId → rootId rootIdCache: Map<string, string>, // profileId → rootId
}; };
private readonly REFRESH_INTERVAL = 10 * 60 * 1000; // 10 นาที private readonly CACHE_TTL = 60 * 60 * 1000; // 60 นาที
} }
``` ```
**การทำงาน:** **การทำงาน:**
- Cache `currentRevision` ทุก 10 นาที - Passive cache refresh - ตรวจสอบและ refresh cache เมื่อมีการเข้าถึงข้อมูล (on-access)
- หาก cache เก่าเกิน 60 นาที จะทำการ refresh อัตโนมัติ
- Lazy load `profileCache` และ `rootIdCache` (โหลดเมื่อถูกเรียกใช้) - Lazy load `profileCache` และ `rootIdCache` (โหลดเมื่อถูกเรียกใช้)
- Method `getProfileByKeycloak()` - ดึง profile จาก cache หรือ database - Method `getProfileByKeycloak()` - ดึง profile จาก cache หรือ database
- Method `getRootIdByProfileId()` - ดึง rootId จาก cache หรือ database - Method `getRootIdByProfileId()` - ดึง rootId จาก cache หรือ database
- ไม่มี setInterval (ลดการใช้งาน timer)
#### 3.2 Log Middleware (`src/middlewares/logs.ts`) #### 3.2 Log Middleware (`src/middlewares/logs.ts`)

View file

@ -17,10 +17,8 @@ class LogMemoryStore {
rootIdCache: new Map(), rootIdCache: new Map(),
updatedAt: new Date(), updatedAt: new Date(),
}; };
private readonly REFRESH_INTERVAL = 10 * 60 * 1000; // 10 minutes private readonly CACHE_TTL = 60 * 60 * 1000; // 60 minutes
private isRefreshing = false;
private isInitialized = false; private isInitialized = false;
private refreshTimer: NodeJS.Timeout | null = null;
constructor() { constructor() {
// ไม่ refresh ทันที - รอให้เรียก initialize() หลัง TypeORM ready // ไม่ refresh ทันที - รอให้เรียก initialize() หลัง TypeORM ready
@ -35,24 +33,15 @@ class LogMemoryStore {
this.isInitialized = true; this.isInitialized = true;
this.refreshCache(); this.refreshCache();
this.refreshTimer = setInterval(() => {
this.refreshCache();
}, this.REFRESH_INTERVAL);
console.log( console.log(
"[LogMemoryStore] Initialized with", "[LogMemoryStore] Initialized with",
this.REFRESH_INTERVAL / 1000, this.CACHE_TTL / 1000 / 60,
"second refresh interval", "minute TTL (passive cleanup on access)",
); );
} }
private async refreshCache() { private async refreshCache() {
if (this.isRefreshing) {
console.log("[LogMemoryStore] Already refreshing, skipping...");
return;
}
this.isRefreshing = true;
try { try {
// Refresh revision cache // Refresh revision cache
const repoRevision = AppDataSource.getRepository(OrgRevision); const repoRevision = AppDataSource.getRepository(OrgRevision);
@ -72,12 +61,26 @@ class LogMemoryStore {
console.log("[LogMemoryStore] Cache refreshed at", this.cache.updatedAt.toISOString()); console.log("[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 { }
this.isRefreshing = false; }
// Check if cache is stale and refresh if needed
private async checkAndRefreshIfNeeded() {
const now = new Date();
const age = now.getTime() - this.cache.updatedAt.getTime();
if (age > this.CACHE_TTL) {
console.log(
"[LogMemoryStore] Cache is stale (age:",
Math.round(age / 1000 / 60),
"minutes), refreshing...",
);
await this.refreshCache();
} }
} }
getCurrentRevision(): OrgRevision | null { getCurrentRevision(): OrgRevision | null {
// Check for stale data (fire and forget)
this.checkAndRefreshIfNeeded();
return this.cache.currentRevision; return this.cache.currentRevision;
} }
@ -89,6 +92,9 @@ class LogMemoryStore {
* Get Profile by keycloak ID with caching * Get Profile by keycloak ID with caching
*/ */
async getProfileByKeycloak(keycloak: string): Promise<Profile | null> { async getProfileByKeycloak(keycloak: string): Promise<Profile | null> {
// Check for stale data
await this.checkAndRefreshIfNeeded();
// Check cache first // Check cache first
if (this.cache.profileCache.has(keycloak)) { if (this.cache.profileCache.has(keycloak)) {
return this.cache.profileCache.get(keycloak)!; return this.cache.profileCache.get(keycloak)!;
@ -114,6 +120,9 @@ class LogMemoryStore {
async getRootIdByProfileId(profileId: string | undefined): Promise<string | null> { async getRootIdByProfileId(profileId: string | undefined): Promise<string | null> {
if (!profileId) return null; if (!profileId) return null;
// Check for stale data
await this.checkAndRefreshIfNeeded();
// Check cache first // Check cache first
if (this.cache.rootIdCache.has(profileId)) { if (this.cache.rootIdCache.has(profileId)) {
return this.cache.rootIdCache.get(profileId)!; return this.cache.rootIdCache.get(profileId)!;
@ -148,10 +157,8 @@ class LogMemoryStore {
// สำหรับ shutdown // สำหรับ shutdown
destroy() { destroy() {
if (this.refreshTimer) { // No active timer to clear
clearInterval(this.refreshTimer); console.log("[LogMemoryStore] Destroyed");
this.refreshTimer = null;
}
} }
} }