fix(moderation): immediately abort retries on 429 Too Many Requests

- In llmModerationClient.ts (inner retry), if OpenAI throws a 429 (or 401/403), throw p-retry's AbortError to immediately exit the 3-attempt inner retry loop.
- In aiAnalyzer.ts (outer retry), propagate the AbortError from runModerationAnalysis so the 2-attempt outer retry loop also aborts immediately.
- This ensures that a burst of 20 concurrent tasks hitting rate limits immediately returns the messages to the DB queue (as 'analysis_incomplete') and rapidly increments the individual circuit breaker, pausing processing and preventing a thundering herd instead of making 12 API calls per stuck message.
This commit is contained in:
MythEclipse
2026-05-28 01:09:57 +07:00
parent 9976e66ca5
commit c6af313c33
2 changed files with 107 additions and 73 deletions
+17
View File
@@ -1,5 +1,6 @@
import { existsSync } from "node:fs"; import { existsSync } from "node:fs";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { AbortError } from "p-retry";
import { Piscina } from "piscina"; import { Piscina } from "piscina";
import { config } from "../config.js"; import { config } from "../config.js";
import { createChildLogger } from "../logger.js"; import { createChildLogger } from "../logger.js";
@@ -227,6 +228,7 @@ async function processIndividualFallback(
const analysisResult = await retryWithBackoff( const analysisResult = await retryWithBackoff(
async () => { async () => {
try {
const result = await runModerationAnalysis({ const result = await runModerationAnalysis({
targets: [message], targets: [message],
contextText: contextLines.join("\n"), contextText: contextLines.join("\n"),
@@ -248,7 +250,22 @@ async function processIndividualFallback(
// Got a real result — clear the incomplete flag. // Got a real result — clear the incomplete flag.
exhaustedOnIncomplete = false; exhaustedOnIncomplete = false;
return result; return result;
} catch (err: any) {
// Propagate AbortError so outer retry is immediately cancelled on 429.
if (err instanceof AbortError) {
throw err;
}
if (
err?.status === 429 ||
err?.status === 401 ||
err?.status === 403
) {
throw new AbortError(err);
}
throw err;
}
}, },
{ {
retries: 2, retries: 2,
+18 -1
View File
@@ -1,4 +1,5 @@
import OpenAI from "openai"; import OpenAI from "openai";
import { AbortError } from "p-retry";
import { z } from "zod"; import { z } from "zod";
import { config } from "../config.js"; import { config } from "../config.js";
import { createChildLogger } from "../logger.js"; import { createChildLogger } from "../logger.js";
@@ -34,7 +35,10 @@ const openai = new OpenAI({
// Override headers to bypass Cloudflare WAF Bot Fight Mode // Override headers to bypass Cloudflare WAF Bot Fight Mode
const headers = new Headers(init?.headers); const headers = new Headers(init?.headers);
headers.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"); headers.set(
"User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
);
for (const key of Array.from(headers.keys())) { for (const key of Array.from(headers.keys())) {
if (key.toLowerCase().startsWith("x-stainless")) { if (key.toLowerCase().startsWith("x-stainless")) {
headers.delete(key); headers.delete(key);
@@ -610,6 +614,7 @@ CRITICAL: "message_id" HARUS berupa STRING (dibungkus tanda kutip ganda). Jangan
try { try {
const analysis = await retryWithBackoff( const analysis = await retryWithBackoff(
async () => { async () => {
try {
const completion = await openai.chat.completions.create({ const completion = await openai.chat.completions.create({
model: config.AI_LLM_MODEL, model: config.AI_LLM_MODEL,
messages: [ messages: [
@@ -666,6 +671,18 @@ CRITICAL: "message_id" HARUS berupa STRING (dibungkus tanda kutip ganda). Jangan
); );
throw parseError; throw parseError;
} }
} catch (apiError: any) {
// Immediately abort retries on rate limits or auth errors so the
// message can return to the DB queue instead of bursting retries.
if (
apiError?.status === 429 ||
apiError?.status === 401 ||
apiError?.status === 403
) {
throw new AbortError(apiError);
}
throw apiError;
}
}, },
{ {
retries: 3, retries: 3,