@@ -478,6 +478,114 @@ func TestDualWriter_Read_NoShadowInDualWrite(t *testing.T) {
478478 assert .Equal (t , 0 , secondary .CallCount (), "no shadow in dual-write mode" )
479479}
480480
481+ type timeoutCapturingBackend struct {
482+ name string
483+ timeout time.Duration
484+ args []any
485+ doCalls int
486+ doWithCalls int
487+ returnValue any
488+ returnErr error
489+ }
490+
491+ func (b * timeoutCapturingBackend ) Do (ctx context.Context , args ... any ) * redis.Cmd {
492+ b .doCalls ++
493+ b .args = append ([]any (nil ), args ... )
494+ cmd := redis .NewCmd (ctx , args ... )
495+ if b .returnErr != nil {
496+ cmd .SetErr (b .returnErr )
497+ return cmd
498+ }
499+ cmd .SetVal (b .returnValue )
500+ return cmd
501+ }
502+
503+ func (b * timeoutCapturingBackend ) DoWithTimeout (ctx context.Context , timeout time.Duration , args ... any ) * redis.Cmd {
504+ b .doWithCalls ++
505+ b .timeout = timeout
506+ b .args = append ([]any (nil ), args ... )
507+ cmd := redis .NewCmd (ctx , args ... )
508+ if b .returnErr != nil {
509+ cmd .SetErr (b .returnErr )
510+ return cmd
511+ }
512+ cmd .SetVal (b .returnValue )
513+ return cmd
514+ }
515+
516+ func (b * timeoutCapturingBackend ) Pipeline (ctx context.Context , cmds [][]any ) ([]* redis.Cmd , error ) {
517+ results := make ([]* redis.Cmd , len (cmds ))
518+ for i , args := range cmds {
519+ results [i ] = b .Do (ctx , args ... )
520+ }
521+ return results , nil
522+ }
523+
524+ func (b * timeoutCapturingBackend ) Close () error { return nil }
525+ func (b * timeoutCapturingBackend ) Name () string { return b .name }
526+
527+ func TestBlockingCommandTimeout (t * testing.T ) {
528+ tests := []struct {
529+ name string
530+ cmd string
531+ args [][]byte
532+ expected time.Duration
533+ }{
534+ {
535+ name : "BZPOPMIN seconds" ,
536+ cmd : "BZPOPMIN" ,
537+ args : [][]byte {[]byte ("BZPOPMIN" ), []byte ("queue" ), []byte ("5" )},
538+ expected : 5 * time .Second ,
539+ },
540+ {
541+ name : "BLMOVE float seconds" ,
542+ cmd : "BLMOVE" ,
543+ args : [][]byte {[]byte ("BLMOVE" ), []byte ("src" ), []byte ("dst" ), []byte ("LEFT" ), []byte ("RIGHT" ), []byte ("2.5" )},
544+ expected : 2500 * time .Millisecond ,
545+ },
546+ {
547+ name : "XREAD block milliseconds" ,
548+ cmd : "XREAD" ,
549+ args : [][]byte {[]byte ("XREAD" ), []byte ("BLOCK" ), []byte ("1500" ), []byte ("STREAMS" ), []byte ("jobs" ), []byte ("0" )},
550+ expected : 1500 * time .Millisecond ,
551+ },
552+ {
553+ name : "XREADGROUP block zero" ,
554+ cmd : "XREADGROUP" ,
555+ args : [][]byte {[]byte ("XREADGROUP" ), []byte ("GROUP" ), []byte ("g" ), []byte ("c" ), []byte ("BLOCK" ), []byte ("0" ), []byte ("STREAMS" ), []byte ("jobs" ), []byte (">" )},
556+ expected : 0 ,
557+ },
558+ {
559+ name : "missing block falls back to zero" ,
560+ cmd : "XREAD" ,
561+ args : [][]byte {[]byte ("XREAD" ), []byte ("STREAMS" ), []byte ("jobs" ), []byte ("0" )},
562+ expected : 0 ,
563+ },
564+ }
565+
566+ for _ , tt := range tests {
567+ t .Run (tt .name , func (t * testing.T ) {
568+ assert .Equal (t , tt .expected , blockingCommandTimeout (tt .cmd , tt .args ))
569+ })
570+ }
571+ }
572+
573+ func TestDualWriter_Blocking_UsesTimeoutAwareBackend (t * testing.T ) {
574+ primary := & timeoutCapturingBackend {name : "primary" , returnValue : "OK" }
575+ secondary := newMockBackend ("secondary" )
576+
577+ metrics := newTestMetrics ()
578+ d := NewDualWriter (primary , secondary , ProxyConfig {Mode : ModeRedisOnly }, metrics , newTestSentry (), testLogger )
579+
580+ resp , err := d .Blocking (context .Background (), "BZPOPMIN" , [][]byte {[]byte ("BZPOPMIN" ), []byte ("queue" ), []byte ("5" )})
581+ assert .NoError (t , err )
582+ assert .Equal (t , "OK" , resp )
583+ assert .Equal (t , 0 , primary .doCalls )
584+ assert .Equal (t , 1 , primary .doWithCalls )
585+ assert .Equal (t , 5 * time .Second , primary .timeout )
586+ assert .Equal (t , []any {[]byte ("BZPOPMIN" ), []byte ("queue" ), []byte ("5" )}, primary .args )
587+ }
588+
481589func TestDualWriter_GoAsync_Bounded (t * testing.T ) {
482590 primary := newMockBackend ("primary" )
483591 primary .doFunc = makeCmd ("OK" , nil )
0 commit comments