Skip to content

Commit 91ce2d1

Browse files
fix(guardrails): bound-parallelize mask batch; refresh stale comments
- maskPIIBatch runs per-string sidecar calls with bounded concurrency (8) via mapWithConcurrency, so a chunk of many small leaves finishes within the 45s request timeout instead of aborting and scrubbing; order + fail-on-error kept - drop stale comments referencing the deleted Python venv / 30s subprocess timeout
1 parent 2df826f commit 91ce2d1

3 files changed

Lines changed: 23 additions & 21 deletions

File tree

apps/sim/app/api/guardrails/mask-batch/route.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ const logger = createLogger('GuardrailsMaskBatchAPI')
1111

1212
/**
1313
* Internal batch PII masking. The log-redaction persist path runs in both the
14-
* Next.js server and the trigger.dev runtime, but Presidio (Python venv) lives
15-
* only in the app container — so redaction calls this endpoint server-to-server
16-
* (internal JWT) to keep Presidio centralized here.
14+
* Next.js server and the trigger.dev runtime, but the Presidio sidecars live only
15+
* in the app task — so redaction calls this endpoint server-to-server (internal
16+
* JWT) to keep Presidio centralized here.
1717
*/
1818
export const POST = withRouteHandler(async (request: NextRequest) => {
1919
const auth = await checkInternalAuth(request, { requireWorkflowId: false })
@@ -31,8 +31,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
3131
logger.info('Masked PII batch', { count: texts.length })
3232
return NextResponse.json({ masked })
3333
} catch (error) {
34-
// A broken/absent venv makes maskPIIBatch throw; fail loudly here (the
35-
// caller scrubs to REDACTION_FAILED, so PII is never leaked).
34+
// An unreachable/misconfigured Presidio sidecar makes maskPIIBatch throw; fail
35+
// loudly here (the caller scrubs to REDACTION_FAILED, so PII is never leaked).
3636
logger.error('PII batch masking failed', {
3737
error: getErrorMessage(error),
3838
count: texts.length,

apps/sim/lib/guardrails/mask-client.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@ import { getInternalApiBaseUrl } from '@/lib/core/utils/urls'
1010
*/
1111
const REQUEST_MAX_BYTES = 256 * 1024
1212
const REQUEST_MAX_COUNT = 2_000
13-
/** Slightly above the 30s Python subprocess timeout so a hung app container aborts gracefully. */
13+
/** Bounds one mask-batch request; an unreachable/stuck Presidio sidecar aborts so the caller scrubs. */
1414
const REQUEST_TIMEOUT_MS = 45_000
1515

1616
/**
1717
* Mask PII across many strings via the internal app-container endpoint.
1818
*
19-
* Presidio (a Python venv) only exists in the app container, but the
20-
* log-redaction persist path also runs inside the trigger.dev runtime — so
21-
* redaction always routes through HTTP, the same way the guardrails tool does.
19+
* The Presidio sidecars run only in the app task, but the log-redaction persist
20+
* path also runs inside the trigger.dev runtime — so redaction always routes
21+
* through HTTP, the same way the guardrails tool does.
2222
* Strings are grouped into byte/count-budgeted chunks; order is preserved, so
2323
* the returned array matches `texts` length.
2424
*

apps/sim/lib/guardrails/validate_pii.ts

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import { createLogger } from '@sim/logger'
22
import { getErrorMessage } from '@sim/utils/errors'
33
import { env } from '@/lib/core/config/env'
4+
import { mapWithConcurrency } from '@/lib/core/utils/concurrency'
45
import { CUSTOM_ENTITY_TYPES, CUSTOM_RECOGNIZERS } from '@/lib/guardrails/recognizers'
56

67
const logger = createLogger('PIIValidator')
78

89
/** Just above the analyzer's spaCy NER budget so a stuck sidecar aborts gracefully. */
910
const REQUEST_TIMEOUT_MS = 45_000
1011

12+
/** Concurrent per-string sidecar calls within one batch; the warm model handles parallelism. */
13+
const MASK_CONCURRENCY = 8
14+
1115
const ANALYZER_URL = env.PRESIDIO_ANALYZER_URL || 'http://localhost:5002'
1216
const ANONYMIZER_URL = env.PRESIDIO_ANONYMIZER_URL || 'http://localhost:5001'
1317

@@ -177,9 +181,12 @@ export async function validatePII(input: PIIValidationInput): Promise<PIIValidat
177181

178182
/**
179183
* Mask PII across many strings via the Presidio sidecars, preserving input order.
180-
* Each string runs a TS VIN pre-pass, then analyze → anonymize. Strings with no
181-
* detected PII are returned unchanged. Rejects on any sidecar failure so callers
182-
* can apply their own fail-safe (scrub rather than leak).
184+
* Each string runs a TS custom-recognizer pass, then analyze → anonymize. Strings
185+
* with no detected PII are returned unchanged. Calls run with bounded concurrency:
186+
* the sidecars' model is warm, so the bottleneck is round-trip latency, and a
187+
* batch of thousands of small leaves would otherwise exceed the caller's request
188+
* timeout if run strictly sequentially. Rejects on any sidecar failure (which
189+
* fails the whole batch) so callers can apply their own fail-safe (scrub).
183190
*/
184191
export async function maskPIIBatch(
185192
texts: string[],
@@ -188,16 +195,11 @@ export async function maskPIIBatch(
188195
): Promise<string[]> {
189196
if (texts.length === 0) return []
190197

191-
const masked: string[] = []
192-
for (const text of texts) {
193-
if (!text) {
194-
masked.push(text)
195-
continue
196-
}
198+
return mapWithConcurrency(texts, MASK_CONCURRENCY, async (text) => {
199+
if (!text) return text
197200
const spans = await collectSpans(text, entityTypes, language)
198-
masked.push(await anonymize(text, spans))
199-
}
200-
return masked
201+
return anonymize(text, spans)
202+
})
201203
}
202204

203205
export { type PIIEntityType, SUPPORTED_PII_ENTITIES } from '@/lib/guardrails/pii-entities'

0 commit comments

Comments
 (0)