161 lines
4.8 KiB
TypeScript
161 lines
4.8 KiB
TypeScript
// services/dbCleanup.ts
|
|
// Database cleanup module for state keys - SQL-only version
|
|
import { handleDatabaseError } from "../modules/KvCacheStore.ts";
|
|
import {
|
|
sqlDeleteStateKey,
|
|
sqlGetAllStateKeys,
|
|
} from "../db/sqlUserRepository.ts";
|
|
|
|
const CLEANUP_MINUTE = 10 * 1000; // 10 seconds between cleanup runs
|
|
const AGE_OUT_TIME = 60 * 60 * 1000; // 1 hour - delete keys older than this
|
|
const DUPLICATE_KEY_AGE = 5 * 60 * 1000; // 5 minutes - keep only newest key per state_id within this window
|
|
|
|
/**
|
|
* Singleton class to ensure only one cleanup instance runs at a time
|
|
* across all user requests on the server.
|
|
*/
|
|
class StateKeyCleanupManager {
|
|
private static instance: StateKeyCleanupManager | null = null;
|
|
private lastCleanupTimestamp = 0;
|
|
private isRunning = false;
|
|
private cleanupPromise: Promise<void> | null = null;
|
|
|
|
// Private constructor to prevent direct instantiation
|
|
private constructor() {
|
|
}
|
|
|
|
/**
|
|
* Get the singleton instance of the cleanup manager
|
|
*/
|
|
public static getInstance(): StateKeyCleanupManager {
|
|
if (!StateKeyCleanupManager.instance) {
|
|
StateKeyCleanupManager.instance = new StateKeyCleanupManager();
|
|
}
|
|
return StateKeyCleanupManager.instance;
|
|
}
|
|
|
|
public async runCleanup(): Promise<void> {
|
|
const now = Date.now();
|
|
|
|
// Check if cleanup was run recently
|
|
if (
|
|
this.lastCleanupTimestamp &&
|
|
now - this.lastCleanupTimestamp < CLEANUP_MINUTE
|
|
) {
|
|
return;
|
|
}
|
|
|
|
// Mutex lock: If cleanup is already running, return the existing promise
|
|
if (this.isRunning && this.cleanupPromise) {
|
|
return this.cleanupPromise;
|
|
}
|
|
// Set mutex lock
|
|
this.isRunning = true;
|
|
|
|
try {
|
|
// Create cleanup promise
|
|
this.cleanupPromise = this.performCleanup(now);
|
|
|
|
await this.cleanupPromise;
|
|
} finally {
|
|
// Release mutex lock
|
|
this.isRunning = false;
|
|
this.cleanupPromise = null;
|
|
}
|
|
}
|
|
|
|
private async performCleanup(now: number): Promise<void> {
|
|
this.lastCleanupTimestamp = now;
|
|
|
|
try {
|
|
// Get all state keys from SQL database
|
|
const allStateKeys = await sqlGetAllStateKeys();
|
|
|
|
if (allStateKeys.length === 0) {
|
|
return;
|
|
}
|
|
|
|
// Group by state_id to identify multiple keys per session
|
|
const stateIdMap = new Map<string, typeof allStateKeys>();
|
|
|
|
for (const key of allStateKeys) {
|
|
const stateId = key.state_id;
|
|
if (!stateIdMap.has(stateId)) {
|
|
stateIdMap.set(stateId, []);
|
|
}
|
|
stateIdMap.get(stateId)!.push(key);
|
|
}
|
|
|
|
let deletedCount = 0;
|
|
|
|
// Process each state_id group
|
|
for (const [stateId, keys] of stateIdMap.entries()) {
|
|
// Sort keys by created_on (newest first)
|
|
keys.sort((a, b) => b.created_on - a.created_on);
|
|
|
|
// Keep the newest key, delete older ones for this state_id
|
|
// This handles state key rotation - old rotated keys are cleaned up
|
|
for (let i = 1; i < keys.length; i++) {
|
|
const oldKey = keys[i];
|
|
const age = now - oldKey.created_on;
|
|
|
|
// Delete old rotated keys (older than 5 minutes)
|
|
if (age > DUPLICATE_KEY_AGE) {
|
|
try {
|
|
await sqlDeleteStateKey(oldKey.seal_key);
|
|
deletedCount++;
|
|
console.log(
|
|
`Cleaned up old state key for ${stateId}, age: ${
|
|
Math.floor(age / 1000)
|
|
}s`,
|
|
);
|
|
} catch (error) {
|
|
console.error("Error deleting old state key:", error);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check the newest (current) key
|
|
const currentKey = keys[0];
|
|
const currentAge = now - currentKey.created_on;
|
|
|
|
// Delete if older than 1 hour (expired session)
|
|
if (currentAge > AGE_OUT_TIME) {
|
|
try {
|
|
await sqlDeleteStateKey(currentKey.seal_key);
|
|
deletedCount++;
|
|
console.log(
|
|
`Cleaned up expired state key for ${stateId}, age: ${
|
|
Math.floor(currentAge / 1000)
|
|
}s`,
|
|
);
|
|
} catch (error) {
|
|
console.error("Error deleting expired state key:", error);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (deletedCount > 0) {
|
|
console.log(
|
|
`State key cleanup completed: ${deletedCount} keys deleted`,
|
|
);
|
|
}
|
|
} catch (error) {
|
|
console.error("Error during state key cleanup:", error);
|
|
// Reset the flag on error so cleanup can be retried
|
|
this.lastCleanupTimestamp = 0;
|
|
handleDatabaseError(error);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Exported function that uses the singleton instance to run cleanup.
|
|
* This ensures only one cleanup runs at a time across all user requests.
|
|
*/
|
|
export function runStateKeyCleanup(): Promise<void> {
|
|
const manager = StateKeyCleanupManager.getInstance();
|
|
return manager.runCleanup();
|
|
}
|