@@ -37,6 +37,7 @@ type warmingReadsVCursor struct {
3737 * loggingVCursor
3838 warmingReadsPercent int
3939 warmingReadsChannel chan bool
40+ warmingReadsTimeout time.Duration
4041 warmingReadsExecuteFunc func (context.Context , Primitive , []* srvtopo.ResolvedShard , []* querypb.BoundQuery , bool , bool )
4142}
4243
@@ -49,16 +50,26 @@ func (vc *warmingReadsVCursor) GetWarmingReadsChannel() chan bool {
4950}
5051
5152func (vc * warmingReadsVCursor ) CloneForReplicaWarming (ctx context.Context ) VCursor {
53+ clonedLogging := & loggingVCursor {
54+ shards : vc .shards ,
55+ results : vc .results ,
56+ onResolveDestinationsFn : vc .onResolveDestinationsFn ,
57+ }
5258 clone := & warmingReadsVCursor {
53- loggingVCursor : vc . loggingVCursor ,
59+ loggingVCursor : clonedLogging ,
5460 warmingReadsPercent : vc .warmingReadsPercent ,
5561 warmingReadsChannel : vc .warmingReadsChannel ,
62+ warmingReadsTimeout : vc .warmingReadsTimeout ,
5663 warmingReadsExecuteFunc : vc .warmingReadsExecuteFunc ,
5764 }
5865 clone .onExecuteMultiShardFn = vc .warmingReadsExecuteFunc
5966 return clone
6067}
6168
69+ func (vc * warmingReadsVCursor ) WarmingReadsContext (ctx context.Context ) (context.Context , context.CancelFunc ) {
70+ return context .WithTimeout (context .Background (), vc .warmingReadsTimeout )
71+ }
72+
6273func TestWarmingReadsSkipsForUpdate (t * testing.T ) {
6374 vindex , _ := vindexes .CreateVindex ("hash" , "" , nil )
6475 testCases := []struct {
@@ -129,29 +140,160 @@ func TestWarmingReadsSkipsForUpdate(t *testing.T) {
129140
130141 var warmingReadExecuted atomic.Bool
131142 var capturedQuery string
143+ var capturedCtxHasDeadline atomic.Bool
144+ var capturedCtxErr atomic.Pointer [error ]
145+ var resolveDestCtxHasDeadline atomic.Bool
146+ // done is closed by the test to unblock the warming read goroutine
147+ // after context assertions have been made.
148+ done := make (chan struct {})
132149 vc := & warmingReadsVCursor {
133150 loggingVCursor : & loggingVCursor {
134151 shards : []string {"-20" , "20-" },
135152 results : []* sqltypes.Result {defaultSelectResult },
153+ onResolveDestinationsFn : func (ctx context.Context ) {
154+ _ , hasDeadline := ctx .Deadline ()
155+ resolveDestCtxHasDeadline .Store (hasDeadline )
156+ },
136157 },
137158 warmingReadsPercent : 100 ,
138159 warmingReadsChannel : make (chan bool , 1 ),
160+ warmingReadsTimeout : 5 * time .Second ,
139161 }
140162 vc .warmingReadsExecuteFunc = func (ctx context.Context , primitive Primitive , rss []* srvtopo.ResolvedShard , queries []* querypb.BoundQuery , rollbackOnError , canAutocommit bool ) {
141163 if len (queries ) > 0 {
142164 capturedQuery = queries [0 ].Sql
143165 }
166+ _ , hasDeadline := ctx .Deadline ()
167+ capturedCtxHasDeadline .Store (hasDeadline )
168+ ctxErr := ctx .Err ()
169+ capturedCtxErr .Store (& ctxErr )
144170 warmingReadExecuted .Store (true )
171+ // Block until the test has checked our context assertions,
172+ // preventing defer cancel() from running.
173+ select {
174+ case <- done :
175+ case <- t .Context ().Done ():
176+ }
145177 }
146178
147- _ , err := route .TryExecute (t .Context (), vc , map [string ]* querypb.BindVariable {}, false )
179+ // Use a cancelable parent context to verify the warming read
180+ // context is independent of the parent request context.
181+ parentCtx , parentCancel := context .WithCancel (t .Context ())
182+ _ , err := route .TryExecute (parentCtx , vc , map [string ]* querypb.BindVariable {}, false )
148183 require .NoError (t , err )
149184
185+ // Cancel the parent context to simulate the primary request completing.
186+ parentCancel ()
187+
150188 require .Eventually (t , func () bool {
151189 return warmingReadExecuted .Load ()
152190 }, time .Second , 10 * time .Millisecond , "warming read should be executed" )
153191
154192 require .Equal (t , tc .expectedWarmingQuery , capturedQuery , "warming read query should match expected" )
193+ require .True (t , capturedCtxHasDeadline .Load (), "warming read context should have a deadline from the timeout" )
194+
195+ // The warming read context should still be active even though the
196+ // parent request context was canceled.
197+ require .NoError (t , * capturedCtxErr .Load (), "warming read context should not be canceled when parent context is canceled" )
198+
199+ // Verify findRoute received the warming context (with deadline), not the parent context.
200+ require .True (t , resolveDestCtxHasDeadline .Load (), "ResolveDestinations should receive a context with deadline from the warming timeout" )
201+
202+ // Unblock the warming read goroutine.
203+ close (done )
155204 })
156205 }
157206}
207+
208+ func TestWarmingReadsDroppedWhenChannelFull (t * testing.T ) {
209+ vindex , _ := vindexes .CreateVindex ("hash" , "" , nil )
210+ route := NewRoute (
211+ EqualUnique ,
212+ & vindexes.Keyspace {
213+ Name : "ks" ,
214+ Sharded : true ,
215+ },
216+ "SELECT * FROM users WHERE id = 1" ,
217+ "dummy_select_field" ,
218+ )
219+ parser , _ := sqlparser .NewTestParser ().Parse ("SELECT * FROM users WHERE id = 1" )
220+ route .QueryStatement = parser
221+ route .Vindex = vindex .(vindexes.SingleColumn )
222+ route .Values = []evalengine.Expr {
223+ evalengine .NewLiteralInt (1 ),
224+ }
225+
226+ var warmingReadExecuted atomic.Bool
227+ vc := & warmingReadsVCursor {
228+ loggingVCursor : & loggingVCursor {
229+ shards : []string {"-20" , "20-" },
230+ results : []* sqltypes.Result {defaultSelectResult },
231+ },
232+ warmingReadsPercent : 100 ,
233+ warmingReadsChannel : make (chan bool , 1 ),
234+ warmingReadsTimeout : 5 * time .Second ,
235+ }
236+ vc .warmingReadsExecuteFunc = func (ctx context.Context , primitive Primitive , rss []* srvtopo.ResolvedShard , queries []* querypb.BoundQuery , rollbackOnError , canAutocommit bool ) {
237+ warmingReadExecuted .Store (true )
238+ }
239+
240+ // Pre-fill the channel to simulate a full pool.
241+ vc .warmingReadsChannel <- true
242+
243+ _ , err := route .TryExecute (t .Context (), vc , map [string ]* querypb.BindVariable {}, false )
244+ require .NoError (t , err )
245+
246+ // Verify over a short window that no warming read is executed while the channel is full.
247+ require .Never (t , func () bool {
248+ return warmingReadExecuted .Load ()
249+ }, 100 * time .Millisecond , 5 * time .Millisecond , "warming read should not execute when the channel is full" )
250+ // Drain the channel.
251+ <- vc .warmingReadsChannel
252+ }
253+
254+ func TestWarmingReadsContextTimeout (t * testing.T ) {
255+ vindex , _ := vindexes .CreateVindex ("hash" , "" , nil )
256+ route := NewRoute (
257+ EqualUnique ,
258+ & vindexes.Keyspace {
259+ Name : "ks" ,
260+ Sharded : true ,
261+ },
262+ "SELECT * FROM users WHERE id = 1" ,
263+ "dummy_select_field" ,
264+ )
265+ parser , _ := sqlparser .NewTestParser ().Parse ("SELECT * FROM users WHERE id = 1" )
266+ route .QueryStatement = parser
267+ route .Vindex = vindex .(vindexes.SingleColumn )
268+ route .Values = []evalengine.Expr {
269+ evalengine .NewLiteralInt (1 ),
270+ }
271+
272+ var capturedCtxErr atomic.Pointer [error ]
273+ var warmingReadExecuted atomic.Bool
274+ vc := & warmingReadsVCursor {
275+ loggingVCursor : & loggingVCursor {
276+ shards : []string {"-20" , "20-" },
277+ results : []* sqltypes.Result {defaultSelectResult },
278+ },
279+ warmingReadsPercent : 100 ,
280+ warmingReadsChannel : make (chan bool , 1 ),
281+ warmingReadsTimeout : 1 * time .Millisecond ,
282+ }
283+ vc .warmingReadsExecuteFunc = func (ctx context.Context , primitive Primitive , rss []* srvtopo.ResolvedShard , queries []* querypb.BoundQuery , rollbackOnError , canAutocommit bool ) {
284+ // Block until the warming context times out.
285+ <- ctx .Done ()
286+ ctxErr := ctx .Err ()
287+ capturedCtxErr .Store (& ctxErr )
288+ warmingReadExecuted .Store (true )
289+ }
290+
291+ _ , err := route .TryExecute (t .Context (), vc , map [string ]* querypb.BindVariable {}, false )
292+ require .NoError (t , err )
293+
294+ require .Eventually (t , func () bool {
295+ return warmingReadExecuted .Load ()
296+ }, time .Second , 10 * time .Millisecond , "warming read should have been executed and timed out" )
297+
298+ require .ErrorIs (t , * capturedCtxErr .Load (), context .DeadlineExceeded , "warming read context should have timed out" )
299+ }
0 commit comments