Skip to content

Commit 0102c05

Browse files
d-csclaude
andcommitted
fix(webapp): batch items bypass the mollifier gate
Batch triggers crash with HTTP 500 "Foreign key constraint violated on the constraint BatchTaskRunItem_taskRunId_fkey" whenever the mollifier gate is active. The crash chain: 1. batchTriggerV3 generates a run id per item, calls `triggerTaskService.call` with `options.batchId` set. 2. The mollifier gate trips, the item is buffered, the mollify response returns a stripped run shape `{ id, friendlyId, spanId }` with no PG row. 3. batchTriggerV3 then tries `prisma.batchTaskRunItem.create({ taskRunId: result.run.id, ... })` — the FK to TaskRun.id fails because no row was written. The proper fix requires both ends: skip the join-row create at trigger-time AND create it on the drainer-side materialise. The drainer fix is non-trivial (the drainer / replay PR territory, not the dashboard PR) and the snapshot already carries `batch: { id, index }` so it has the info — but until that's wired through to a `BatchTaskRunItem.create` call on materialise, skipping trigger-time would silently lose the batch <-> run link forever and break batch progress reporting + `batchTriggerAndWait` parent resumption. Short-circuit the gate when `options.batchId` is set so batch items always go straight to PG. Batch triggers lose the burst-protection benefit of the mollifier; single triggers and triggerAndWait are unaffected. Removing this bypass is the natural follow-up once the drainer-side BatchTaskRunItem path lands in the appropriate earlier PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1e6af43 commit 0102c05

1 file changed

Lines changed: 43 additions & 16 deletions

File tree

apps/webapp/app/runEngine/services/triggerTask.server.ts

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -386,22 +386,49 @@ export class RunEngineTriggerTaskService {
386386
// `evaluateGate` can also override the gate-on check (the
387387
// default reads `env.TRIGGER_MOLLIFIER_ENABLED`, which is "0"
388388
// in CI where no .env file is present).
389-
const mollifierOutcome: GateOutcome | null = this.isMollifierGloballyEnabled()
390-
? await this.evaluateGate({
391-
envId: environment.id,
392-
orgId: environment.organizationId,
393-
taskId,
394-
orgFeatureFlags:
395-
(environment.organization.featureFlags as Record<string, unknown> | null) ??
396-
null,
397-
options: {
398-
debounce: body.options?.debounce,
399-
oneTimeUseToken: options.oneTimeUseToken,
400-
parentTaskRunId: body.options?.parentRunId,
401-
resumeParentOnCompletion: body.options?.resumeParentOnCompletion,
402-
},
403-
})
404-
: null;
389+
//
390+
// Batch items bypass the mollifier gate entirely.
391+
//
392+
// The mollify path returns a stripped run-shape `{ id,
393+
// friendlyId, spanId }` with no PG row written. Batch
394+
// tracking relies on `BatchTaskRunItem`, a join row whose
395+
// `taskRunId` column has a NOT NULL FK to `TaskRun.id` —
396+
// creating that join at trigger-time (in
397+
// `batchTriggerV3.server.ts:871`) fails with FK violation
398+
// for any mollified item, and skipping it at trigger-time
399+
// would silently drop the batch↔run link forever because
400+
// the drainer's materialise path doesn't (yet) create
401+
// `BatchTaskRunItem`. Either side alone is wrong:
402+
// - skip at trigger-time only → batch progress
403+
// under-reports forever, `batchTriggerAndWait` parent
404+
// stays parked
405+
// - mollify at trigger-time only → FK violation, 500
406+
//
407+
// The proper end state is a drainer-side
408+
// `BatchTaskRunItem` create-on-materialise (the snapshot
409+
// already carries `batch: { id, index }` so the drainer
410+
// has the info). That belongs in the drainer / replay PR,
411+
// not here. Until that lands, batch triggers pass-through
412+
// — they lose the burst-protection benefit, but the path
413+
// works end-to-end.
414+
const skipMollifierForBatch = !!options.batchId;
415+
const mollifierOutcome: GateOutcome | null =
416+
this.isMollifierGloballyEnabled() && !skipMollifierForBatch
417+
? await this.evaluateGate({
418+
envId: environment.id,
419+
orgId: environment.organizationId,
420+
taskId,
421+
orgFeatureFlags:
422+
(environment.organization.featureFlags as Record<string, unknown> | null) ??
423+
null,
424+
options: {
425+
debounce: body.options?.debounce,
426+
oneTimeUseToken: options.oneTimeUseToken,
427+
parentTaskRunId: body.options?.parentRunId,
428+
resumeParentOnCompletion: body.options?.resumeParentOnCompletion,
429+
},
430+
})
431+
: null;
405432

406433
// When the gate says mollify, write the engine.trigger input
407434
// snapshot into the Redis buffer and return a synthesised

0 commit comments

Comments
 (0)