Files
dcrouter/ts/cache/classes.cache.cleaner.ts

167 lines
4.6 KiB
TypeScript

import * as plugins from '../plugins.js';
import { logger } from '../logger.js';
import { CacheDb } from './classes.cachedb.js';
// Import document classes for cleanup
import { CachedEmail } from './documents/classes.cached.email.js';
import { CachedIPReputation } from './documents/classes.cached.ip.reputation.js';
/**
* Configuration for the cache cleaner
*/
export interface ICacheCleanerOptions {
/** Cleanup interval in milliseconds (default: 1 hour) */
intervalMs?: number;
/** Enable verbose logging */
verbose?: boolean;
}
/**
* CacheCleaner - Periodically removes expired documents from the cache
*
* Runs on a configurable interval (default: hourly) and queries each
* collection for documents where expiresAt < now(), then deletes them.
*/
export class CacheCleaner {
private cleanupInterval: ReturnType<typeof setInterval> | null = null;
private isRunning: boolean = false;
private options: Required<ICacheCleanerOptions>;
private cacheDb: CacheDb;
constructor(cacheDb: CacheDb, options: ICacheCleanerOptions = {}) {
this.cacheDb = cacheDb;
this.options = {
intervalMs: options.intervalMs || 60 * 60 * 1000, // 1 hour default
verbose: options.verbose || false,
};
}
/**
* Start the periodic cleanup process
*/
public start(): void {
if (this.isRunning) {
logger.log('warn', 'CacheCleaner already running');
return;
}
this.isRunning = true;
// Run cleanup immediately on start
this.runCleanup().catch((error) => {
logger.log('error', `Initial cache cleanup failed: ${error.message}`);
});
// Schedule periodic cleanup
this.cleanupInterval = setInterval(() => {
this.runCleanup().catch((error) => {
logger.log('error', `Cache cleanup failed: ${error.message}`);
});
}, this.options.intervalMs);
logger.log(
'info',
`CacheCleaner started with interval: ${this.options.intervalMs / 1000 / 60} minutes`
);
}
/**
* Stop the periodic cleanup process
*/
public stop(): void {
if (!this.isRunning) {
return;
}
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
this.cleanupInterval = null;
}
this.isRunning = false;
logger.log('info', 'CacheCleaner stopped');
}
/**
* Run a single cleanup cycle
*/
public async runCleanup(): Promise<void> {
if (!this.cacheDb.isReady()) {
logger.log('warn', 'CacheDb not ready, skipping cleanup');
return;
}
const now = new Date();
const results: { collection: string; deleted: number }[] = [];
try {
const emailsDeleted = await this.cleanExpiredDocuments(CachedEmail, now);
results.push({ collection: 'CachedEmail', deleted: emailsDeleted });
const ipReputationDeleted = await this.cleanExpiredDocuments(CachedIPReputation, now);
results.push({ collection: 'CachedIPReputation', deleted: ipReputationDeleted });
// Log results
const totalDeleted = results.reduce((sum, r) => sum + r.deleted, 0);
if (totalDeleted > 0 || this.options.verbose) {
const summary = results
.filter((r) => r.deleted > 0)
.map((r) => `${r.collection}: ${r.deleted}`)
.join(', ');
logger.log(
'info',
`Cache cleanup completed. Deleted ${totalDeleted} expired documents. ${summary || 'No deletions.'}`
);
}
} catch (error) {
logger.log('error', `Cache cleanup error: ${error.message}`);
throw error;
}
}
/**
* Clean expired documents from a specific collection using smartdata API
*/
private async cleanExpiredDocuments<T extends { delete: () => Promise<void> }>(
documentClass: { getInstances: (filter: any) => Promise<T[]> },
now: Date
): Promise<number> {
try {
// Find all expired documents
const expiredDocs = await documentClass.getInstances({
expiresAt: { $lt: now },
});
// Delete each expired document
let deletedCount = 0;
for (const doc of expiredDocs) {
try {
await doc.delete();
deletedCount++;
} catch (deleteError) {
logger.log('warn', `Failed to delete expired document: ${deleteError.message}`);
}
}
return deletedCount;
} catch (error) {
logger.log('error', `Error cleaning collection: ${error.message}`);
return 0;
}
}
/**
* Check if the cleaner is running
*/
public isActive(): boolean {
return this.isRunning;
}
/**
* Get the cleanup interval in milliseconds
*/
public getIntervalMs(): number {
return this.options.intervalMs;
}
}