feat(ai-moderation): add circuit breaker alert system with extensible handlers
This commit is contained in:
@@ -87,7 +87,7 @@ function isAgeRestrictedMessage(message: MessageRecord): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildAgeRestrictedSkipResult(): {
|
function buildAgeRestrictedSkipResult(): {
|
||||||
status: "clean";
|
status: "error";
|
||||||
flags: string | null;
|
flags: string | null;
|
||||||
score: number;
|
score: number;
|
||||||
analysis: string;
|
analysis: string;
|
||||||
@@ -99,7 +99,7 @@ function buildAgeRestrictedSkipResult(): {
|
|||||||
error: null;
|
error: null;
|
||||||
} {
|
} {
|
||||||
return {
|
return {
|
||||||
status: "clean",
|
status: "error",
|
||||||
flags: JSON.stringify(["age_restricted"]),
|
flags: JSON.stringify(["age_restricted"]),
|
||||||
score: 0,
|
score: 0,
|
||||||
analysis: "Skipped moderation for age-restricted content.",
|
analysis: "Skipped moderation for age-restricted content.",
|
||||||
@@ -157,6 +157,38 @@ const conversationConsecutiveErrors = new Map<string, number>();
|
|||||||
const MAX_CONSECUTIVE_ERRORS = 5;
|
const MAX_CONSECUTIVE_ERRORS = 5;
|
||||||
const CONVERSATION_CB_COOLDOWN_MS = 60000;
|
const CONVERSATION_CB_COOLDOWN_MS = 60000;
|
||||||
|
|
||||||
|
/** Alert sinks — called when circuit breakers or sustained errors fire. */
|
||||||
|
type CircuitBreakerAlert = {
|
||||||
|
type: "conversation_cb" | "individual_cb" | "sustained_error";
|
||||||
|
conversationKey?: string;
|
||||||
|
consecutiveErrors: number;
|
||||||
|
message: string;
|
||||||
|
lastError?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Registered alert handlers */
|
||||||
|
const alertHandlers: Array<(alert: CircuitBreakerAlert) => void> = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register an alert handler (e.g., for webhook integration).
|
||||||
|
*/
|
||||||
|
export function onCircuitBreakerAlert(
|
||||||
|
handler: (alert: CircuitBreakerAlert) => void,
|
||||||
|
): void {
|
||||||
|
alertHandlers.push(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fireAlert(alert: CircuitBreakerAlert): void {
|
||||||
|
logger.warn(alert, `CB Alert: ${alert.type} — ${alert.message}`);
|
||||||
|
for (const handler of alertHandlers) {
|
||||||
|
try {
|
||||||
|
handler(alert);
|
||||||
|
} catch {
|
||||||
|
// handler errors are non-critical
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function recordConversationBatchFailure(conversationKey: string): void {
|
function recordConversationBatchFailure(conversationKey: string): void {
|
||||||
const nextCount =
|
const nextCount =
|
||||||
(conversationConsecutiveErrors.get(conversationKey) ?? 0) + 1;
|
(conversationConsecutiveErrors.get(conversationKey) ?? 0) + 1;
|
||||||
@@ -167,10 +199,14 @@ function recordConversationBatchFailure(conversationKey: string): void {
|
|||||||
conversationKey,
|
conversationKey,
|
||||||
Date.now() + CONVERSATION_CB_COOLDOWN_MS,
|
Date.now() + CONVERSATION_CB_COOLDOWN_MS,
|
||||||
);
|
);
|
||||||
logger.warn(
|
fireAlert({
|
||||||
{ conversationKey, consecutiveErrors: nextCount },
|
type: "conversation_cb",
|
||||||
"Conversation circuit breaker triggered due to consecutive batch errors",
|
conversationKey,
|
||||||
);
|
consecutiveErrors: nextCount,
|
||||||
|
message: `Conversation ${conversationKey} circuit breaker triggered after ${nextCount} consecutive errors`,
|
||||||
|
lastError,
|
||||||
|
});
|
||||||
|
conversationConsecutiveErrors.set(conversationKey, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,13 +490,12 @@ async function processIndividualFallback(
|
|||||||
individualConsecutiveErrors >= config.AI_ANALYSIS_INDIVIDUAL_CB_THRESHOLD
|
individualConsecutiveErrors >= config.AI_ANALYSIS_INDIVIDUAL_CB_THRESHOLD
|
||||||
) {
|
) {
|
||||||
individualCooldownUntil = Date.now() + INDIVIDUAL_COOLDOWN_MS;
|
individualCooldownUntil = Date.now() + INDIVIDUAL_COOLDOWN_MS;
|
||||||
logger.warn(
|
fireAlert({
|
||||||
{
|
type: "individual_cb",
|
||||||
threshold: config.AI_ANALYSIS_INDIVIDUAL_CB_THRESHOLD,
|
consecutiveErrors: individualConsecutiveErrors,
|
||||||
cooldownUntil: new Date(individualCooldownUntil).toISOString(),
|
message: `Individual fallback circuit breaker triggered after ${individualConsecutiveErrors} consecutive errors`,
|
||||||
},
|
lastError,
|
||||||
"Individual fallback circuit breaker triggered",
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lastError = error instanceof Error ? error.message : String(error);
|
lastError = error instanceof Error ? error.message : String(error);
|
||||||
|
|||||||
Reference in New Issue
Block a user