@@ -11,6 +11,10 @@ import { findRunByIdWithMollifierFallback } from "~/v3/mollifier/readFallback.se
1111import { claimOrAwait } from "~/v3/mollifier/idempotencyClaim.server" ;
1212import { makeResolveMollifierFlag } from "~/v3/mollifier/mollifierGate.server" ;
1313import { runStore } from "~/v3/runStore.server" ;
14+ import { runOpsLegacyPrisma , runOpsNewPrisma } from "~/db.server" ;
15+ import { isSplitEnabled } from "~/v3/runOpsMigration/splitMode.server" ;
16+ import { resolveRunIdMintKind } from "~/v3/engineVersion.server" ;
17+ import { resolveIdempotencyDedupClient } from "./idempotencyResidency.server" ;
1418import type { TraceEventConcern , TriggerTaskRequest } from "../types" ;
1519
1620// In-memory per-org mollifier-enabled check, shared with `evaluateGate`
@@ -147,6 +151,28 @@ export class IdempotencyKeyConcern {
147151 return { isCached : false , idempotencyKey, idempotencyKeyExpiresAt } ;
148152 }
149153
154+ // Probe and clears must hit the DB where the would-be run will physically live.
155+ const dedupClient = await resolveIdempotencyDedupClient (
156+ {
157+ environmentForMint : {
158+ organizationId : request . environment . organizationId ,
159+ id : request . environment . id ,
160+ orgFeatureFlags : request . environment . organization ?. featureFlags ,
161+ } ,
162+ parentRunFriendlyId : request . body . options ?. parentRunId ,
163+ } ,
164+ {
165+ isSplitEnabled,
166+ fallbackClient : this . prisma ,
167+ newClient : runOpsNewPrisma ,
168+ legacyClient : runOpsLegacyPrisma ,
169+ resolveMintKind : resolveRunIdMintKind ,
170+ // `isMigrated` is intentionally omitted: until a child of a swept
171+ // legacy-id parent can be born on the new DB, the swept-marker override
172+ // would never change the answer, so a child routes by parent id-shape.
173+ }
174+ ) ;
175+
150176 const existingRun = idempotencyKey
151177 ? await runStore . findRun (
152178 {
@@ -159,7 +185,7 @@ export class IdempotencyKeyConcern {
159185 associatedWaitpoint : true ,
160186 } ,
161187 } ,
162- this . prisma
188+ dedupClient
163189 )
164190 : undefined ;
165191
@@ -193,7 +219,7 @@ export class IdempotencyKeyConcern {
193219 // Update the existing run to remove the idempotency key
194220 await runStore . clearIdempotencyKey (
195221 { byId : { runId : existingRun . id , idempotencyKey } } ,
196- this . prisma
222+ dedupClient
197223 ) ;
198224
199225 return { isCached : false , idempotencyKey, idempotencyKeyExpiresAt } ;
@@ -210,7 +236,7 @@ export class IdempotencyKeyConcern {
210236 // Update the existing run to remove the idempotency key
211237 await runStore . clearIdempotencyKey (
212238 { byId : { runId : existingRun . id , idempotencyKey } } ,
213- this . prisma
239+ dedupClient
214240 ) ;
215241
216242 return { isCached : false , idempotencyKey, idempotencyKeyExpiresAt } ;
@@ -249,7 +275,6 @@ export class IdempotencyKeyConcern {
249275 ? `${ event . traceparent . spanId } :${ event . spanId } `
250276 : event . spanId ;
251277
252- //block run with waitpoint
253278 await this . engine . blockRunWithWaitpoint ( {
254279 runId : RunId . fromFriendlyId ( parentRunId ) ,
255280 waitpoints : associatedWaitpoint ! . id ,
@@ -277,24 +302,13 @@ export class IdempotencyKeyConcern {
277302 // (resumeParentOnCompletion) — that path bypasses the gate entirely
278303 // and its existing PG-side dedup is sufficient.
279304 //
280- // Also gated on the same per-org mollifier flag the gate uses: when
281- // `TRIGGER_MOLLIFIER_ENABLED=1` globally for staged rollout, the buffer
282- // singleton is constructed and `claimOrAwait` would otherwise issue a
283- // Redis SETNX for EVERY idempotency-keyed trigger — including orgs
284- // that haven't opted in. Those orgs never enter the mollify branch
285- // (the gate always returns pass_through for them), so there's no
286- // buffer activity to serialise against; PG's unique constraint
287- // already deduplicates concurrent same-key races. Resolving the org
288- // flag is a pure in-memory read of `Organization.featureFlags` — no
289- // DB query, same predicate the gate uses — keeping the claim's Redis
290- // RTT off the hot path for non-opted-in orgs during incremental
291- // rollout.
292- // Match the gate's bypass list (`mollifierGate.server.ts:158-175`).
293- // debounce + oneTimeUseToken triggers always return pass_through from
294- // the gate, so claiming a Redis SETNX here is wasted RTT on the
295- // trigger hot path. Excluding them keeps the claim aligned with the
296- // gate — if the gate would never mollify the request, there's no
297- // buffer to serialise against.
305+ // Gated on the same per-org mollifier flag the gate uses, and the same
306+ // bypass list (debounce + oneTimeUseToken): if the gate would never mollify
307+ // the request, there's no buffer to serialise against and PG's unique
308+ // constraint already deduplicates concurrent same-key races. Skipping the
309+ // claim's Redis SETNX keeps its RTT off the hot path for those requests
310+ // during staged rollout. The org-flag check is a pure in-memory read of
311+ // `Organization.featureFlags`, no DB query.
298312 const claimEligible =
299313 ! request . body . options ?. resumeParentOnCompletion &&
300314 ! request . body . options ?. debounce &&
@@ -336,7 +350,7 @@ export class IdempotencyKeyConcern {
336350 taskIdentifier : request . taskId ,
337351 } ,
338352 { include : { associatedWaitpoint : true } } ,
339- this . prisma
353+ dedupClient
340354 ) ;
341355 if ( writerRun ) {
342356 return { isCached : true , run : writerRun } ;
@@ -350,27 +364,18 @@ export class IdempotencyKeyConcern {
350364 if ( buffered ) {
351365 return { isCached : true , run : buffered } ;
352366 }
353- // Claim resolved to a runId nothing can find — the run was
354- // genuinely lost (claimant errored after publish, drain failed,
355- // or both the PG row and buffer entry TTL'd out). This is
356- // terminal, not transient: `lookupIdempotency` self-heals a
357- // dangling pointer, and `ack` keeps the entry hash as a
358- // read-fallback past the PG write, so re-polling cannot conjure
359- // a run that is gone. Falling through to a fresh trigger is the
360- // correct recovery.
367+ // Claim resolved to a runId nothing can find — the run was genuinely
368+ // lost (claimant errored after publish, or both the PG row and buffer
369+ // entry TTL'd out). Terminal, not transient, so falling through to a
370+ // fresh trigger is the correct recovery.
361371 //
362- // Why falling through claimless is safe (no duplicate runs):
363- // concurrent triggers that also fall through here converge on a
364- // single run via the same dedup backstops the claim layer relies
365- // on — the PG unique constraint on the idempotency key
366- // (RunDuplicateIdempotencyKeyError → retry resolves to the
367- // winner) for the pass-through path, and `accept`'s idempotency
368- // SETNX (`duplicate_idempotency`) for the mollify path. Once the
369- // first fall-through commits a run, later callers find it via the
370- // writer-PG / buffer lookups above despite the stale `resolved:`
371- // slot, which the slot's TTL clears within ~30s. The residual
372- // cost is a few redundant (deduped) trigger attempts in that
373- // window, not duplicate runs.
372+ // Falling through claimless doesn't duplicate runs: concurrent
373+ // fall-throughs converge on one run via the same dedup backstops the
374+ // claim layer relies on — PG's unique constraint on the idempotency key
375+ // (pass-through path) and `accept`'s SETNX (mollify path). Once the
376+ // first commits, later callers find it via the writer-PG / buffer
377+ // lookups above despite the stale `resolved:` slot (cleared by its ~30s
378+ // TTL). Residual cost is a few deduped trigger attempts, not dup runs.
374379 logger . warn ( "idempotency claim resolved but runId not findable" , {
375380 envId : request . environment . id ,
376381 taskIdentifier : request . taskId ,
0 commit comments