Circuit breaker

This commit is contained in:
Joey Yakimowich-Payne 2026-02-03 08:35:10 -07:00
commit b7126b0d07
3 changed files with 234 additions and 3 deletions

View file

@ -11,6 +11,40 @@ const PROCESSING_TIMEOUT_MS = 30000;
const SANDBOX_URL = process.env.SANDBOX_URL || 'http://localhost:3002';
const USE_SANDBOX = process.env.USE_SANDBOX !== 'false';
const CIRCUIT_BREAKER_THRESHOLD = 5;
const CIRCUIT_BREAKER_RESET_MS = 60000;
class CircuitBreaker {
private failures = 0;
private lastFailure = 0;
private open = false;
recordSuccess(): void {
this.failures = 0;
this.open = false;
}
recordFailure(): void {
this.failures++;
this.lastFailure = Date.now();
if (this.failures >= CIRCUIT_BREAKER_THRESHOLD) {
this.open = true;
}
}
isOpen(): boolean {
if (!this.open) return false;
if (Date.now() - this.lastFailure > CIRCUIT_BREAKER_RESET_MS) {
this.open = false;
this.failures = 0;
return false;
}
return true;
}
}
const sandboxCircuit = new CircuitBreaker();
export const GEMINI_NATIVE_TYPES = [
'application/pdf',
'text/plain',
@ -210,6 +244,10 @@ const LEGACY_TO_MODERN: Record<string, string> = {
};
async function convertViaSandbox(buffer: Buffer, extension: string): Promise<Buffer> {
if (sandboxCircuit.isOpen()) {
throw new Error('CIRCUIT_OPEN');
}
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), PROCESSING_TIMEOUT_MS);
@ -223,14 +261,20 @@ async function convertViaSandbox(buffer: Buffer, extension: string): Promise<Buf
if (!response.ok) {
const error = await response.json().catch(() => ({ error: 'Conversion failed' }));
sandboxCircuit.recordFailure();
throw new Error(error.error || 'Document conversion failed');
}
sandboxCircuit.recordSuccess();
return Buffer.from(await response.arrayBuffer());
} catch (err) {
if (err instanceof Error && err.name === 'AbortError') {
sandboxCircuit.recordFailure();
throw new Error('Document conversion timed out. Try a smaller file.');
}
if (err instanceof Error && err.message !== 'CIRCUIT_OPEN') {
sandboxCircuit.recordFailure();
}
throw err;
} finally {
clearTimeout(timeout);
@ -282,9 +326,20 @@ async function convertLocally(buffer: Buffer, extension: string): Promise<Buffer
}
async function extractWithLibreOffice(buffer: Buffer, extension: string, useOcr: boolean = false): Promise<string> {
const convertedBuffer = USE_SANDBOX
? await convertViaSandbox(buffer, extension)
: await convertLocally(buffer, extension);
let convertedBuffer: Buffer;
if (USE_SANDBOX) {
try {
convertedBuffer = await convertViaSandbox(buffer, extension);
} catch (err) {
if (err instanceof Error && err.message === 'CIRCUIT_OPEN') {
throw new Error('Document conversion service temporarily unavailable. Please try again in a minute.');
}
throw err;
}
} else {
convertedBuffer = await convertLocally(buffer, extension);
}
const config = useOcr ? { extractAttachments: true, ocr: true, ocrLanguage: 'eng' } : {};
const ast = await officeParser.parseOffice(convertedBuffer, config);