@@ -6,7 +6,11 @@ vi.mock("~/db.server", () => ({
66} ) ) ;
77
88import { mutateWithFallback } from "~/v3/mollifier/mutateWithFallback.server" ;
9- import type { MollifierBuffer , MutateSnapshotResult } from "@trigger.dev/redis-worker" ;
9+ import type {
10+ BufferEntry ,
11+ MollifierBuffer ,
12+ MutateSnapshotResult ,
13+ } from "@trigger.dev/redis-worker" ;
1014import type { TaskRun } from "@trigger.dev/database" ;
1115
1216type FindFirst = ReturnType < typeof vi . fn > ;
@@ -22,9 +26,30 @@ function fakePrisma(rows: Array<TaskRun | null>): PrismaStub {
2226function bufferReturning ( result : MutateSnapshotResult ) : MollifierBuffer {
2327 return {
2428 mutateSnapshot : vi . fn ( async ( ) => result ) ,
29+ getEntry : vi . fn ( async ( ) => null ) ,
2530 } as unknown as MollifierBuffer ;
2631}
2732
33+ // Buffer whose mutateSnapshot returns "busy" and whose getEntry walks a
34+ // scripted sequence of entry states (the drainer's progress). The last
35+ // element repeats once the sequence is exhausted.
36+ function bufferBusy ( entries : Array < BufferEntry | null > ) : MollifierBuffer {
37+ const getEntry = vi . fn ( ) ;
38+ for ( const e of entries ) getEntry . mockResolvedValueOnce ( e ) ;
39+ getEntry . mockResolvedValue ( entries . length ? entries [ entries . length - 1 ] : null ) ;
40+ return {
41+ mutateSnapshot : vi . fn ( async ( ) => "busy" as const ) ,
42+ getEntry,
43+ } as unknown as MollifierBuffer ;
44+ }
45+
46+ const entryDraining = ( ) : BufferEntry =>
47+ ( { status : "DRAINING" , materialised : false } ) as unknown as BufferEntry ;
48+ const entryQueued = ( ) : BufferEntry =>
49+ ( { status : "QUEUED" , materialised : false } ) as unknown as BufferEntry ;
50+ const entryMaterialised = ( ) : BufferEntry =>
51+ ( { status : "DRAINING" , materialised : true } ) as unknown as BufferEntry ;
52+
2853const fakeRun = ( overrides : Partial < TaskRun > = { } ) : TaskRun =>
2954 ( {
3055 id : "pg_id" ,
@@ -101,55 +126,138 @@ describe("mutateWithFallback", () => {
101126 expect ( pgMutation ) . toHaveBeenCalledWith ( row ) ;
102127 } ) ;
103128
104- it ( "replica miss + buffer busy + writer resolves mid-wait → pgMutation " , async ( ) => {
129+ it ( "busy → watches buffer through DRAINING, materialises, hits primary exactly once " , async ( ) => {
105130 const row = fakeRun ( ) ;
106131 const pgMutation = vi . fn ( async ( ) => "pg-after-wait" ) ;
107- // Replica misses; writer misses twice, then hits.
108- const writer = fakePrisma ( [ null , null , row ] ) ;
132+ // Writer is read ONCE, only after the buffer reports materialised.
133+ const writer = fakePrisma ( [ row ] ) ;
134+ const buffer = bufferBusy ( [ entryDraining ( ) , entryDraining ( ) , entryMaterialised ( ) ] ) ;
109135 let nowValue = 0 ;
110136 const result = await mutateWithFallback ( {
111137 ...baseInput ,
112138 pgMutation,
113139 synthesisedResponse : ( ) => "snap" ,
114140 prismaReplica : fakePrisma ( [ null ] ) as unknown as typeof import ( "~/db.server" ) . $replica ,
115141 prismaWriter : writer as unknown as typeof import ( "~/db.server" ) . prisma ,
116- getBuffer : ( ) => bufferReturning ( "busy" ) ,
117- sleep : async ( ) => {
118- nowValue += 20 ;
142+ getBuffer : ( ) => buffer ,
143+ sleep : async ( ms ) => {
144+ nowValue += ms ;
119145 } ,
120146 now : ( ) => nowValue ,
121147 safetyNetMs : 2000 ,
122148 pollStepMs : 20 ,
123- pgTimeoutMs : 50 ,
149+ random : ( ) => 0 ,
124150 } ) ;
125151 expect ( result ) . toEqual ( { kind : "pg" , response : "pg-after-wait" } ) ;
126152 expect ( pgMutation ) . toHaveBeenCalledWith ( row ) ;
127- // Writer should have been polled 3 times before the hit.
128- expect ( writer . taskRun . findFirst ) . toHaveBeenCalledTimes ( 3 ) ;
153+ // Detection happened against Redis (3 polls), the primary exactly once.
154+ expect ( buffer . getEntry ) . toHaveBeenCalledTimes ( 3 ) ;
155+ expect ( writer . taskRun . findFirst ) . toHaveBeenCalledTimes ( 1 ) ;
156+ } ) ;
157+
158+ it ( "busy → entry deleted by terminal fail, writer finds SYSTEM_FAILURE row → pgMutation" , async ( ) => {
159+ const row = fakeRun ( ) ;
160+ const pgMutation = vi . fn ( async ( ) => "pg-failed-row" ) ;
161+ const writer = fakePrisma ( [ row ] ) ;
162+ const buffer = bufferBusy ( [ entryDraining ( ) , null ] ) ;
163+ let nowValue = 0 ;
164+ const result = await mutateWithFallback ( {
165+ ...baseInput ,
166+ pgMutation,
167+ synthesisedResponse : ( ) => "snap" ,
168+ prismaReplica : fakePrisma ( [ null ] ) as unknown as typeof import ( "~/db.server" ) . $replica ,
169+ prismaWriter : writer as unknown as typeof import ( "~/db.server" ) . prisma ,
170+ getBuffer : ( ) => buffer ,
171+ sleep : async ( ms ) => {
172+ nowValue += ms ;
173+ } ,
174+ now : ( ) => nowValue ,
175+ safetyNetMs : 2000 ,
176+ pollStepMs : 20 ,
177+ random : ( ) => 0 ,
178+ } ) ;
179+ expect ( result ) . toEqual ( { kind : "pg" , response : "pg-failed-row" } ) ;
180+ expect ( writer . taskRun . findFirst ) . toHaveBeenCalledTimes ( 1 ) ;
181+ } ) ;
182+
183+ it ( "busy → entry deleted but no PG row (terminal write failed) → not_found" , async ( ) => {
184+ const buffer = bufferBusy ( [ null ] ) ;
185+ const writer = fakePrisma ( [ null ] ) ;
186+ let nowValue = 0 ;
187+ const result = await mutateWithFallback ( {
188+ ...baseInput ,
189+ pgMutation : async ( ) => "pg" ,
190+ synthesisedResponse : ( ) => "snap" ,
191+ prismaReplica : fakePrisma ( [ null ] ) as unknown as typeof import ( "~/db.server" ) . $replica ,
192+ prismaWriter : writer as unknown as typeof import ( "~/db.server" ) . prisma ,
193+ getBuffer : ( ) => buffer ,
194+ sleep : async ( ms ) => {
195+ nowValue += ms ;
196+ } ,
197+ now : ( ) => nowValue ,
198+ safetyNetMs : 2000 ,
199+ pollStepMs : 20 ,
200+ random : ( ) => 0 ,
201+ } ) ;
202+ expect ( result ) . toEqual ( { kind : "not_found" } ) ;
203+ expect ( writer . taskRun . findFirst ) . toHaveBeenCalledTimes ( 1 ) ;
204+ } ) ;
205+
206+ it ( "busy → requeued (back to QUEUED) then materialises; doesn't resolve early" , async ( ) => {
207+ const row = fakeRun ( ) ;
208+ const pgMutation = vi . fn ( async ( ) => "pg-after-requeue" ) ;
209+ const writer = fakePrisma ( [ row ] ) ;
210+ // QUEUED (requeued after a retryable drain error) must NOT be treated
211+ // as "done" — the run hasn't reached PG. Only the later materialise does.
212+ const buffer = bufferBusy ( [ entryQueued ( ) , entryDraining ( ) , entryMaterialised ( ) ] ) ;
213+ let nowValue = 0 ;
214+ const result = await mutateWithFallback ( {
215+ ...baseInput ,
216+ pgMutation,
217+ synthesisedResponse : ( ) => "snap" ,
218+ prismaReplica : fakePrisma ( [ null ] ) as unknown as typeof import ( "~/db.server" ) . $replica ,
219+ prismaWriter : writer as unknown as typeof import ( "~/db.server" ) . prisma ,
220+ getBuffer : ( ) => buffer ,
221+ sleep : async ( ms ) => {
222+ nowValue += ms ;
223+ } ,
224+ now : ( ) => nowValue ,
225+ safetyNetMs : 2000 ,
226+ pollStepMs : 20 ,
227+ random : ( ) => 0 ,
228+ } ) ;
229+ expect ( result ) . toEqual ( { kind : "pg" , response : "pg-after-requeue" } ) ;
230+ expect ( buffer . getEntry ) . toHaveBeenCalledTimes ( 3 ) ;
231+ expect ( writer . taskRun . findFirst ) . toHaveBeenCalledTimes ( 1 ) ;
129232 } ) ;
130233
131- it ( "replica miss + buffer busy + drainer never resolves → timed_out" , async ( ) => {
234+ it ( "busy → drainer never resolves (stays DRAINING) → timed_out, primary never touched" , async ( ) => {
235+ const writer = fakePrisma ( [ ] ) ;
236+ const buffer = bufferBusy ( [ entryDraining ( ) ] ) ;
132237 let nowValue = 0 ;
133238 const result = await mutateWithFallback ( {
134239 ...baseInput ,
135240 pgMutation : async ( ) => "pg" ,
136241 synthesisedResponse : ( ) => "snap" ,
137242 prismaReplica : fakePrisma ( [ null ] ) as unknown as typeof import ( "~/db.server" ) . $replica ,
138- prismaWriter : fakePrisma ( [ null , null , null , null , null ] ) as unknown as typeof import ( "~/db.server" ) . prisma ,
139- getBuffer : ( ) => bufferReturning ( "busy" ) ,
140- sleep : async ( ) => {
141- nowValue += 20 ;
243+ prismaWriter : writer as unknown as typeof import ( "~/db.server" ) . prisma ,
244+ getBuffer : ( ) => buffer ,
245+ sleep : async ( ms ) => {
246+ nowValue += ms ;
142247 } ,
143248 now : ( ) => nowValue ,
144- safetyNetMs : 60 ,
249+ safetyNetMs : 100 ,
145250 pollStepMs : 20 ,
146- pgTimeoutMs : 5 ,
251+ random : ( ) => 0 ,
147252 } ) ;
148253 expect ( result ) . toEqual ( { kind : "timed_out" } ) ;
254+ // The whole point: while the run is still draining we never read the primary.
255+ expect ( writer . taskRun . findFirst ) . toHaveBeenCalledTimes ( 0 ) ;
149256 } ) ;
150257
151258 it ( "abort signal during wait → timed_out without further polls" , async ( ) => {
152- const writer = fakePrisma ( [ null , null , null ] ) ;
259+ const writer = fakePrisma ( [ ] ) ;
260+ const buffer = bufferBusy ( [ entryDraining ( ) , entryDraining ( ) ] ) ;
153261 const controller = new AbortController ( ) ;
154262 let nowValue = 0 ;
155263 const result = await mutateWithFallback ( {
@@ -158,20 +266,21 @@ describe("mutateWithFallback", () => {
158266 synthesisedResponse : ( ) => "snap" ,
159267 prismaReplica : fakePrisma ( [ null ] ) as unknown as typeof import ( "~/db.server" ) . $replica ,
160268 prismaWriter : writer as unknown as typeof import ( "~/db.server" ) . prisma ,
161- getBuffer : ( ) => bufferReturning ( "busy" ) ,
162- sleep : async ( ) => {
163- nowValue += 20 ;
269+ getBuffer : ( ) => buffer ,
270+ sleep : async ( ms ) => {
271+ nowValue += ms ;
164272 controller . abort ( ) ;
165273 } ,
166274 now : ( ) => nowValue ,
167275 safetyNetMs : 2000 ,
168276 pollStepMs : 20 ,
169- pgTimeoutMs : 5 ,
277+ random : ( ) => 0 ,
170278 abortSignal : controller . signal ,
171279 } ) ;
172280 expect ( result ) . toEqual ( { kind : "timed_out" } ) ;
173- // One poll happened before the sleep+abort.
174- expect ( writer . taskRun . findFirst ) . toHaveBeenCalledTimes ( 1 ) ;
281+ // One buffer poll happened before the sleep+abort; primary untouched.
282+ expect ( buffer . getEntry ) . toHaveBeenCalledTimes ( 1 ) ;
283+ expect ( writer . taskRun . findFirst ) . toHaveBeenCalledTimes ( 0 ) ;
175284 } ) ;
176285
177286 it ( "buffer is null (mollifier disabled) → not_found after replica miss" , async ( ) => {
0 commit comments