@@ -8,7 +8,7 @@ use futures::FutureExt;
88use logger_core:: log_info;
99use redis:: aio:: ConnectionLike ;
1010use redis:: cluster_async:: ClusterConnection ;
11- use redis:: cluster_routing:: { RoutingInfo , SingleNodeRoutingInfo } ;
11+ use redis:: cluster_routing:: { Routable , RoutingInfo , SingleNodeRoutingInfo } ;
1212use redis:: RedisResult ;
1313use redis:: { Cmd , ErrorKind , Value } ;
1414pub use standalone_client:: StandaloneClient ;
@@ -98,10 +98,67 @@ async fn run_with_timeout<T>(
9898 timeout : Duration ,
9999 future : impl futures:: Future < Output = RedisResult < T > > + Send ,
100100) -> redis:: RedisResult < T > {
101- tokio:: time:: timeout ( timeout, future)
102- . await
103- . map_err ( |_| io:: Error :: from ( io:: ErrorKind :: TimedOut ) . into ( ) )
104- . and_then ( |res| res)
101+ if timeout == Duration :: from_secs ( 0 ) {
102+ // run without timeout
103+ future. await
104+ } else {
105+ // run with timeout
106+ tokio:: time:: timeout ( timeout, future)
107+ . await
108+ . map_err ( |_| io:: Error :: from ( io:: ErrorKind :: TimedOut ) . into ( ) )
109+ . and_then ( |res| res)
110+ }
111+ }
112+
113+ // Extension to the request timeout for blocking commands to ensure we won't return with timeout error before the server responded
114+ const BLOCKING_CMD_TIMEOUT_EXTENSION : f64 = 0.5 ; // seconds
115+
116+ enum TimeUnit {
117+ Milliseconds = 1000 ,
118+ Seconds = 1 ,
119+ }
120+
121+ // Attempts to get the timeout duration from the command argument at `timeout_idx`.
122+ // If the argument can be parsed into a duration, it returns the duration in seconds. Otherwise, it returns None.
123+ fn try_get_timeout_from_cmd_arg (
124+ cmd : & Cmd ,
125+ timeout_idx : usize ,
126+ time_unit : TimeUnit ,
127+ ) -> Option < Duration > {
128+ cmd. arg_idx ( timeout_idx) . and_then ( |timeout_bytes| {
129+ std:: str:: from_utf8 ( timeout_bytes)
130+ . ok ( )
131+ . and_then ( |timeout_str| {
132+ timeout_str. parse :: < f64 > ( ) . ok ( ) . map ( |timeout| {
133+ let mut timeout_secs = timeout / ( ( time_unit as i32 ) as f64 ) ;
134+ if timeout_secs < 0.0 {
135+ // Timeout cannot be negative, return None so the default request timeout will be used and the server will response with error
136+ return None ;
137+ } else if timeout_secs > 0.0 {
138+ // Extend the request timeout to ensure we don't timeout before receiving a response from the server.
139+ timeout_secs += BLOCKING_CMD_TIMEOUT_EXTENSION ;
140+ } ;
141+ Some ( Duration :: from_secs_f64 ( timeout_secs) )
142+ } )
143+ } )
144+ . unwrap_or ( None )
145+ } )
146+ }
147+
148+ fn get_request_timeout ( cmd : & Cmd , default_timeout : Duration ) -> Duration {
149+ let command = cmd. command ( ) . unwrap_or_default ( ) ;
150+ let blocking_timeout = match command. as_slice ( ) {
151+ b"BLPOP" | b"BRPOP" | b"BLMOVE" | b"BZPOPMAX" | b"BZPOPMIN" | b"BRPOPLPUSH" => {
152+ try_get_timeout_from_cmd_arg ( cmd, cmd. args_iter ( ) . len ( ) - 1 , TimeUnit :: Seconds )
153+ }
154+ b"BLMPOP" | b"BZMPOP" => try_get_timeout_from_cmd_arg ( cmd, 1 , TimeUnit :: Seconds ) ,
155+ b"XREAD" | b"XREADGROUP" => cmd
156+ . position ( b"BLOCK" )
157+ . and_then ( |idx| try_get_timeout_from_cmd_arg ( cmd, idx + 1 , TimeUnit :: Milliseconds ) ) ,
158+ _ => None ,
159+ } ;
160+
161+ blocking_timeout. unwrap_or ( default_timeout)
105162}
106163
107164impl Client {
@@ -111,7 +168,7 @@ impl Client {
111168 routing : Option < RoutingInfo > ,
112169 ) -> redis:: RedisFuture < ' a , Value > {
113170 let expected_type = expected_type_for_cmd ( cmd) ;
114- run_with_timeout ( self . request_timeout , async move {
171+ run_with_timeout ( get_request_timeout ( cmd , self . request_timeout ) , async move {
115172 match self . internal_client {
116173 ClientWrapper :: Standalone ( ref mut client) => client. send_command ( cmd) . await ,
117174
@@ -472,3 +529,121 @@ impl GlideClientForTests for StandaloneClient {
472529 self . send_command ( cmd) . boxed ( )
473530 }
474531}
532+
533+ #[ cfg( test) ]
534+ mod tests {
535+ use std:: time:: Duration ;
536+
537+ use redis:: Cmd ;
538+
539+ use crate :: client:: { get_request_timeout, TimeUnit , BLOCKING_CMD_TIMEOUT_EXTENSION } ;
540+
541+ use super :: try_get_timeout_from_cmd_arg;
542+
543+ #[ test]
544+ fn test_get_timeout_from_cmd_returns_correct_duration_int ( ) {
545+ let mut cmd = Cmd :: new ( ) ;
546+ cmd. arg ( "BLPOP" ) . arg ( "key1" ) . arg ( "key2" ) . arg ( "5" ) ;
547+ assert_eq ! (
548+ try_get_timeout_from_cmd_arg( & cmd, cmd. args_iter( ) . len( ) - 1 , TimeUnit :: Seconds ) ,
549+ Some ( Duration :: from_secs_f64(
550+ 5.0 + BLOCKING_CMD_TIMEOUT_EXTENSION
551+ ) )
552+ ) ;
553+ }
554+
555+ #[ test]
556+ fn test_get_timeout_from_cmd_returns_correct_duration_float ( ) {
557+ let mut cmd = Cmd :: new ( ) ;
558+ cmd. arg ( "BLPOP" ) . arg ( "key1" ) . arg ( "key2" ) . arg ( 0.5 ) ;
559+ assert_eq ! (
560+ try_get_timeout_from_cmd_arg( & cmd, cmd. args_iter( ) . len( ) - 1 , TimeUnit :: Seconds ) ,
561+ Some ( Duration :: from_secs_f64(
562+ 0.5 + BLOCKING_CMD_TIMEOUT_EXTENSION
563+ ) )
564+ ) ;
565+ }
566+
567+ #[ test]
568+ fn test_get_timeout_from_cmd_returns_correct_duration_milliseconds ( ) {
569+ let mut cmd = Cmd :: new ( ) ;
570+ cmd. arg ( "XREAD" ) . arg ( "BLOCK" ) . arg ( "500" ) . arg ( "key" ) ;
571+ assert_eq ! (
572+ try_get_timeout_from_cmd_arg( & cmd, 2 , TimeUnit :: Milliseconds ) ,
573+ Some ( Duration :: from_secs_f64(
574+ 0.5 + BLOCKING_CMD_TIMEOUT_EXTENSION
575+ ) )
576+ ) ;
577+ }
578+
579+ #[ test]
580+ fn test_get_timeout_from_cmd_returns_none_when_timeout_isnt_passed ( ) {
581+ let mut cmd = Cmd :: new ( ) ;
582+ cmd. arg ( "BLPOP" ) . arg ( "key1" ) . arg ( "key2" ) . arg ( "key3" ) ;
583+ assert_eq ! (
584+ try_get_timeout_from_cmd_arg( & cmd, cmd. args_iter( ) . len( ) - 1 , TimeUnit :: Seconds ) ,
585+ None ,
586+ ) ;
587+ }
588+
589+ #[ test]
590+ fn test_get_timeout_from_cmd_returns_none_when_timeout_is_negative ( ) {
591+ let mut cmd = Cmd :: new ( ) ;
592+ cmd. arg ( "BLPOP" ) . arg ( "key1" ) . arg ( "key2" ) . arg ( -1 ) ;
593+ assert_eq ! (
594+ try_get_timeout_from_cmd_arg( & cmd, cmd. args_iter( ) . len( ) - 1 , TimeUnit :: Seconds ) ,
595+ None ,
596+ ) ;
597+ }
598+
599+ #[ test]
600+ fn test_get_timeout_from_cmd_returns_duration_without_extension_when_zero_is_passed ( ) {
601+ let mut cmd = Cmd :: new ( ) ;
602+ cmd. arg ( "BLPOP" ) . arg ( "key1" ) . arg ( "key2" ) . arg ( 0 ) ;
603+ assert_eq ! (
604+ try_get_timeout_from_cmd_arg( & cmd, cmd. args_iter( ) . len( ) - 1 , TimeUnit :: Seconds ) ,
605+ Some ( Duration :: from_secs( 0 ) ) ,
606+ ) ;
607+ }
608+
609+ #[ test]
610+ fn test_get_request_timeout_with_blocking_command_returns_cmd_arg_timeout ( ) {
611+ let mut cmd = Cmd :: new ( ) ;
612+ cmd. arg ( "BLPOP" ) . arg ( "key1" ) . arg ( "key2" ) . arg ( "500" ) ;
613+ assert_eq ! (
614+ get_request_timeout( & cmd, Duration :: from_millis( 100 ) ) ,
615+ Duration :: from_secs_f64( 500.0 + BLOCKING_CMD_TIMEOUT_EXTENSION )
616+ ) ;
617+
618+ let mut cmd = Cmd :: new ( ) ;
619+ cmd. arg ( "XREADGROUP" ) . arg ( "BLOCK" ) . arg ( "500" ) . arg ( "key" ) ;
620+ assert_eq ! (
621+ get_request_timeout( & cmd, Duration :: from_millis( 100 ) ) ,
622+ Duration :: from_secs_f64( 0.5 + BLOCKING_CMD_TIMEOUT_EXTENSION )
623+ ) ;
624+
625+ let mut cmd = Cmd :: new ( ) ;
626+ cmd. arg ( "BLMPOP" ) . arg ( "0.857" ) . arg ( "key" ) ;
627+ assert_eq ! (
628+ get_request_timeout( & cmd, Duration :: from_millis( 100 ) ) ,
629+ Duration :: from_secs_f64( 0.857 + BLOCKING_CMD_TIMEOUT_EXTENSION )
630+ ) ;
631+ }
632+
633+ #[ test]
634+ fn test_get_request_timeout_non_blocking_command_returns_default_timeout ( ) {
635+ let mut cmd = Cmd :: new ( ) ;
636+ cmd. arg ( "SET" ) . arg ( "key" ) . arg ( "value" ) . arg ( "PX" ) . arg ( "500" ) ;
637+ assert_eq ! (
638+ get_request_timeout( & cmd, Duration :: from_millis( 100 ) ) ,
639+ Duration :: from_millis( 100 )
640+ ) ;
641+
642+ let mut cmd = Cmd :: new ( ) ;
643+ cmd. arg ( "XREADGROUP" ) . arg ( "key" ) ;
644+ assert_eq ! (
645+ get_request_timeout( & cmd, Duration :: from_millis( 100 ) ) ,
646+ Duration :: from_millis( 100 )
647+ ) ;
648+ }
649+ }
0 commit comments