feat(backend): mail_classification_samples row-cap (100k max)
Tabelle war auf 13GB gewachsen und hat heute den Disk voll gemacht. Neuer täglicher Row-Cap-Job hält die Tabelle unter 100k Rows — löscht älteste Samples wenn Cap überschritten. CTE-basierter Delete nutzt created_at-Index, kein Full-Table-Scan. Bestehende Jobs bleiben: Subject-Nullification (30 Tage) + Sample-Purge (12 Monate). Row-Cap ist die harte Schranke gegen Disk-Wachstum. 100k Rows ≈ ~500MB — nachhaltig für Staging + Prod. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
53d8ace974
commit
24044c3a0c
@ -1,26 +1,26 @@
|
||||
/**
|
||||
* Mail-Retention-Cron — Phase 1 Data-Foundation.
|
||||
*
|
||||
* Zweck: DSGVO-konforme Datensparsamkeit (Art. 5 Abs. 1e — Speicherbegrenzung).
|
||||
* Zweck: DSGVO-konforme Datensparsamkeit (Art. 5 Abs. 1e — Speicherbegrenzung)
|
||||
* + Disk-Budget-Kontrolle (Staging + Prod).
|
||||
*
|
||||
* Zwei Retention-Jobs:
|
||||
* Drei Retention-Jobs:
|
||||
*
|
||||
* 1. Subject-Nullification (täglich):
|
||||
* Raw-Subject in MailClassificationSample auf null setzen wenn älter als 30 Tage.
|
||||
* Das sanitisierte Subject (features.subjectSanitized) bleibt erhalten —
|
||||
* nur der raw-Text wird entfernt. Begründung: raw-Subject kann noch PII
|
||||
* enthalten die sanitizeSubjectForTraining() übersehen hat (z.B. Names die
|
||||
* nicht ALL-CAPS sind). Nach 30 Tagen ist der Klassifikations-Nutzen gering.
|
||||
* Raw-Subject auf null setzen wenn älter als 30 Tage. Das sanitisierte
|
||||
* Subject (features.subjectSanitized) bleibt erhalten.
|
||||
*
|
||||
* 2. Sample-Purge (monatlich):
|
||||
* MailClassificationSamples älter als 12 Monate werden hart gelöscht.
|
||||
* Begründung: Trainingsdaten werden im Phase-2-Export batch-extrahiert
|
||||
* (Colab). Nach 12 Monaten sind ältere Samples für Modell-Updates weniger
|
||||
* relevant als neue Samples. User-Lösch-Recht (Art. 17) ist davon unabhängig
|
||||
* und wird via deleteUserMailClassificationSamples() behandelt.
|
||||
* Samples älter als 12 Monate hart löschen (für LLM-Training irrelevant).
|
||||
* User-Lösch-Recht (Art. 17) → deleteUserMailClassificationSamples().
|
||||
*
|
||||
* 3. Row-Cap (täglich):
|
||||
* Hält die Tabelle auf MAX_SAMPLE_ROWS. Löscht älteste Rows wenn Cap
|
||||
* überschritten. Verhindert unbegrenztes Disk-Wachstum (Staging + Prod).
|
||||
* Cap = 100k Rows ≈ ~500MB bei durchschnittlicher Row-Größe.
|
||||
*
|
||||
* Läuft nur in Production (import.meta.dev guard).
|
||||
* Beide Jobs laufen beim Server-Start einmalig (initial sweep) + dann periodisch.
|
||||
* Alle Jobs laufen beim Server-Start einmalig + dann periodisch.
|
||||
*/
|
||||
|
||||
import { consola } from "consola";
|
||||
@ -28,22 +28,24 @@ import { usePrisma } from "../utils/prisma";
|
||||
|
||||
// Subject-Nullification: täglich
|
||||
const SUBJECT_NULLIFY_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
||||
// Subject wird nach 30 Tagen auf null gesetzt
|
||||
const SUBJECT_NULLIFY_AGE_DAYS = 30;
|
||||
|
||||
// Sample-Purge: alle 30 Tage (monatlich)
|
||||
const SAMPLE_PURGE_INTERVAL_MS = 30 * 24 * 60 * 60 * 1000;
|
||||
// Samples älter als 12 Monate werden gelöscht
|
||||
const SAMPLE_PURGE_AGE_DAYS = 365;
|
||||
|
||||
// Row-Cap: täglich — hält Tabelle unter diesem Limit
|
||||
const ROW_CAP_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
||||
const MAX_SAMPLE_ROWS = 100_000;
|
||||
|
||||
export default defineNitroPlugin((nitro) => {
|
||||
if (import.meta.dev) return;
|
||||
|
||||
consola.info("[mail-retention-cron] Starting — subject-nullify daily, sample-purge monthly");
|
||||
consola.info("[mail-retention-cron] Starting — subject-nullify daily, sample-purge monthly, row-cap daily");
|
||||
|
||||
// Einmaliger initialer Sweep bei Server-Start (deckt verpasste Runs ab)
|
||||
void runSubjectNullification().catch(() => {});
|
||||
void runSamplePurge().catch(() => {});
|
||||
void runRowCap().catch(() => {});
|
||||
|
||||
const nullifyInterval = setInterval(() => {
|
||||
void runSubjectNullification().catch(() => {});
|
||||
@ -53,9 +55,14 @@ export default defineNitroPlugin((nitro) => {
|
||||
void runSamplePurge().catch(() => {});
|
||||
}, SAMPLE_PURGE_INTERVAL_MS);
|
||||
|
||||
const capInterval = setInterval(() => {
|
||||
void runRowCap().catch(() => {});
|
||||
}, ROW_CAP_INTERVAL_MS);
|
||||
|
||||
nitro.hooks.hook("close", () => {
|
||||
clearInterval(nullifyInterval);
|
||||
clearInterval(purgeInterval);
|
||||
clearInterval(capInterval);
|
||||
});
|
||||
});
|
||||
|
||||
@ -84,7 +91,6 @@ async function runSubjectNullification(): Promise<void> {
|
||||
|
||||
/**
|
||||
* Löscht MailClassificationSamples älter als 12 Monate.
|
||||
* Gibt die Anzahl gelöschter Rows zurück (für Logging).
|
||||
*/
|
||||
async function runSamplePurge(): Promise<void> {
|
||||
const db = usePrisma();
|
||||
@ -98,3 +104,32 @@ async function runSamplePurge(): Promise<void> {
|
||||
consola.info(`[mail-retention-cron] sample-purge: ${result.count} old samples deleted`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Row-Cap: hält mail_classification_samples unter MAX_SAMPLE_ROWS.
|
||||
*
|
||||
* Löscht älteste Rows wenn der Cap überschritten wird. Nutzt einen
|
||||
* CTE-basierten Delete der nur einen Index-Scan braucht (created_at INDEX).
|
||||
* Kein Full-Table-Scan — auch auf großen Tabellen effizient.
|
||||
*/
|
||||
async function runRowCap(): Promise<void> {
|
||||
const db = usePrisma();
|
||||
|
||||
const count = await db.mailClassificationSample.count();
|
||||
if (count <= MAX_SAMPLE_ROWS) return;
|
||||
|
||||
const toDelete = count - MAX_SAMPLE_ROWS;
|
||||
|
||||
// Löscht die `toDelete` ältesten Rows via Subquery auf created_at-Index.
|
||||
const result = await db.$executeRaw`
|
||||
DELETE FROM "rebreak"."mail_classification_samples"
|
||||
WHERE "id" IN (
|
||||
SELECT "id"
|
||||
FROM "rebreak"."mail_classification_samples"
|
||||
ORDER BY "created_at" ASC
|
||||
LIMIT ${toDelete}
|
||||
)
|
||||
`;
|
||||
|
||||
consola.info(`[mail-retention-cron] row-cap: ${result} samples pruned (cap=${MAX_SAMPLE_ROWS}, was=${count})`);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user