perf(mail): kill redundant 30min scan-cron + in-flight scan guard
Backend-Lag-Fix Phase 1 — entlastet die CPU-Dauerschleife im Mail-Stack: - delete mail-scan-cron.ts: der 30-Min-Nitro-Cron scannte alle User parallel (Promise.allSettled) und war redundant zum IMAP-IDLE-Daemon (Single Source of Truth). Reine Dauerlast ohne Mehrwert. - imap-idle: In-Flight-Guard (scanInFlight + coalescePending). triggerScan ist jetzt re-entry-safe — pro Connection max. 1 aktiver + 1 pending Scan statt bis zu 8 gestapelt pro 2-Min-NOOP-Tick. Gilt für NOOP + exists-Event. - plan-features: Pro mailAgents 3->2 (+ Math.min-Hack in coach/message aufgeräumt). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
5531ef5419
commit
5b57bea9c0
@ -477,9 +477,38 @@ const sessions = new Map();
|
|||||||
|
|
||||||
let shuttingDown = false;
|
let shuttingDown = false;
|
||||||
|
|
||||||
// ─── Scan-Trigger ─────────────────────────────────────────────────────────────
|
// ─── In-Flight-Guard ─────────────────────────────────────────────────────────
|
||||||
|
// Verhindert gestapelte scan-internal-Aufrufe für dieselbe Connection.
|
||||||
|
//
|
||||||
|
// scanInFlight: Map<connId, boolean> — läuft gerade ein Scan?
|
||||||
|
// coalescePending: Map<connId, boolean> — kam während des laufenden Scans
|
||||||
|
// ein weiterer Trigger? Wenn ja, einmalig nachholen.
|
||||||
|
//
|
||||||
|
// Verhalten:
|
||||||
|
// - Kein laufender Scan: sofort feuern, inFlight=true setzen
|
||||||
|
// - Läuft Scan bereits: pending=true setzen (coalesce — nicht stapeln)
|
||||||
|
// - Nach Scan-Ende: wenn pending=true → einen weiteren Scan starten,
|
||||||
|
// dann pending=false. Maximal EIN gestapelter Trigger bleibt hängen.
|
||||||
|
//
|
||||||
|
// Gilt für NOOP-Tick UND exists-Event (beide rufen triggerScan auf).
|
||||||
|
|
||||||
|
const scanInFlight = new Map(); // Map<connId, boolean>
|
||||||
|
const coalescePending = new Map(); // Map<connId, boolean>
|
||||||
|
|
||||||
async function triggerScan(conn) {
|
async function triggerScan(conn) {
|
||||||
|
const connId = conn.id;
|
||||||
|
|
||||||
|
if (scanInFlight.get(connId)) {
|
||||||
|
// Scan läuft bereits — coalescer merkt sich den Wunsch, stapelt nicht
|
||||||
|
if (!coalescePending.get(connId)) {
|
||||||
|
log(conn.email, "scan-trigger coalesced (in-flight)");
|
||||||
|
coalescePending.set(connId, true);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
scanInFlight.set(connId, true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${BACKEND_URL}/api/mail/scan-internal`, {
|
const res = await fetch(`${BACKEND_URL}/api/mail/scan-internal`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -500,6 +529,15 @@ async function triggerScan(conn) {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logError(conn.email, "scan-trigger failed", err);
|
logError(conn.email, "scan-trigger failed", err);
|
||||||
|
} finally {
|
||||||
|
scanInFlight.set(connId, false);
|
||||||
|
|
||||||
|
// Coalesced Trigger nachholen: einmalig, fire-and-forget
|
||||||
|
if (coalescePending.get(connId)) {
|
||||||
|
coalescePending.set(connId, false);
|
||||||
|
log(conn.email, "scan-trigger coalesced fire (post-flight)");
|
||||||
|
triggerScan(conn).catch(() => {});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -329,10 +329,7 @@ function generatePlanDetails(): string {
|
|||||||
? "(rückfüllbar – Slot wird wieder frei wenn die Domain global aufgenommen ODER von der Community abgelehnt wurde)"
|
? "(rückfüllbar – Slot wird wieder frei wenn die Domain global aufgenommen ODER von der Community abgelehnt wurde)"
|
||||||
: "(NICHT rückfüllbar – einmal belegt, bleibt für immer belegt)";
|
: "(NICHT rückfüllbar – einmal belegt, bleibt für immer belegt)";
|
||||||
|
|
||||||
// Hinweis: PLAN_LIMITS.pro.mailAgents = 3 widerspricht aktuell dem Briefing
|
const proMailCount = pro.mailAgents;
|
||||||
// (Pro = max 2 Mail-Konten). rebreak-backend muss das angleichen — Lyra
|
|
||||||
// beschreibt hier den Briefing-Stand (2 Konten Pro), damit User korrekt informiert wird.
|
|
||||||
const proMailCount = Math.min(pro.mailAgents, 2);
|
|
||||||
const legendMailFairUse = 10;
|
const legendMailFairUse = 10;
|
||||||
|
|
||||||
return `Pro (3,99 € / Monat — Stripe-Web-Checkout, kein In-App-Kauf):
|
return `Pro (3,99 € / Monat — Stripe-Web-Checkout, kein In-App-Kauf):
|
||||||
|
|||||||
@ -1,37 +0,0 @@
|
|||||||
import { consola } from "consola";
|
|
||||||
import { getAllActiveMailUserIds } from "../db/mail";
|
|
||||||
|
|
||||||
const SCAN_INTERVAL = 30 * 60 * 1000; // every 30 minutes (proxy EXISTS handles real-time)
|
|
||||||
|
|
||||||
export default defineNitroPlugin((nitro) => {
|
|
||||||
if (import.meta.dev) return;
|
|
||||||
|
|
||||||
consola.info("[mail-scan-cron] Starting – scanning due accounts every 30 min");
|
|
||||||
|
|
||||||
const interval = setInterval(runScan, SCAN_INTERVAL);
|
|
||||||
nitro.hooks.hook("close", () => clearInterval(interval));
|
|
||||||
});
|
|
||||||
|
|
||||||
async function runScan() {
|
|
||||||
let userIds: string[];
|
|
||||||
try {
|
|
||||||
userIds = await getAllActiveMailUserIds();
|
|
||||||
} catch {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userIds.length === 0) return;
|
|
||||||
|
|
||||||
const adminSecret = process.env.NUXT_ADMIN_SECRET || process.env.ADMIN_SECRET;
|
|
||||||
if (!adminSecret) return;
|
|
||||||
|
|
||||||
await Promise.allSettled(
|
|
||||||
userIds.map((userId) =>
|
|
||||||
$fetch("/api/mail/scan-internal", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "x-admin-secret": adminSecret },
|
|
||||||
body: { userId },
|
|
||||||
}).catch(() => {})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -82,7 +82,7 @@ export const PLAN_LIMITS: Record<Exclude<Plan, "free">, PlanLimits> = {
|
|||||||
pro: {
|
pro: {
|
||||||
customDomains: 10,
|
customDomains: 10,
|
||||||
domainRefill: true,
|
domainRefill: true,
|
||||||
mailAgents: 3,
|
mailAgents: 2,
|
||||||
mailIntervalOptions: [1, 4, 8],
|
mailIntervalOptions: [1, 4, 8],
|
||||||
globalBlocklist: "full",
|
globalBlocklist: "full",
|
||||||
canPost: true,
|
canPost: true,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user