BREAKING CHANGE(config): convert configuration management to read-only; remove updateConfiguration endpoint and client-side editing
This commit is contained in:
170
ts/cache/classes.cache.cleaner.ts
vendored
Normal file
170
ts/cache/classes.cache.cleaner.ts
vendored
Normal file
@@ -0,0 +1,170 @@
|
||||
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';
|
||||
import { CachedBounce } from './documents/classes.cached.bounce.js';
|
||||
import { CachedSuppression } from './documents/classes.cached.suppression.js';
|
||||
import { CachedDKIMKey } from './documents/classes.cached.dkim.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 {
|
||||
// Clean CachedEmail documents
|
||||
const emailsDeleted = await this.cleanCollection(CachedEmail, now);
|
||||
results.push({ collection: 'CachedEmail', deleted: emailsDeleted });
|
||||
|
||||
// Clean CachedIPReputation documents
|
||||
const ipReputationDeleted = await this.cleanCollection(CachedIPReputation, now);
|
||||
results.push({ collection: 'CachedIPReputation', deleted: ipReputationDeleted });
|
||||
|
||||
// Clean CachedBounce documents
|
||||
const bouncesDeleted = await this.cleanCollection(CachedBounce, now);
|
||||
results.push({ collection: 'CachedBounce', deleted: bouncesDeleted });
|
||||
|
||||
// Clean CachedSuppression documents (but not permanent ones)
|
||||
const suppressionDeleted = await this.cleanCollection(CachedSuppression, now);
|
||||
results.push({ collection: 'CachedSuppression', deleted: suppressionDeleted });
|
||||
|
||||
// Clean CachedDKIMKey documents
|
||||
const dkimDeleted = await this.cleanCollection(CachedDKIMKey, now);
|
||||
results.push({ collection: 'CachedDKIMKey', deleted: dkimDeleted });
|
||||
|
||||
// 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
|
||||
*/
|
||||
private async cleanCollection<T>(
|
||||
documentClass: { deleteMany: (filter: any) => Promise<any> },
|
||||
now: Date
|
||||
): Promise<number> {
|
||||
try {
|
||||
const result = await documentClass.deleteMany({
|
||||
expiresAt: { $lt: now },
|
||||
});
|
||||
return result?.deletedCount || 0;
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user