mirror of https://github.com/redis/redis.git
				
				
				
			When redis-cli received ASK, it didn't handle it (#8930)
When redis-cli received ASK, it used string matching wrong and didn't
handle it.
When we access a slot which is in migrating state, it maybe
return ASK. After redirect to the new node, we need send ASKING
command before retry the command.  In this PR after redis-cli receives
ASK, we send a ASKING command before send the origin command
after reconnecting.
Other changes:
* Make redis-cli -u and -c (unix socket and cluster mode) incompatible
  with one another.
* When send command fails, we avoid the 2nd reconnect retry and just
  print the error info. Users will decide how to do next.
  See #9277.
* Add a test faking two redis nodes in TCL to just send ASK and OK in
  redis protocol to test ASK behavior.
Co-authored-by: Viktor Söderqvist <viktor.soderqvist@est.tech>
Co-authored-by: Oran Agra <oran@redislabs.com>
(cherry picked from commit cf61ad14cc)
			
			
This commit is contained in:
		
							parent
							
								
									9da232c05a
								
							
						
					
					
						commit
						8892b5cf9e
					
				|  | @ -220,6 +220,7 @@ static struct config { | |||
|     long long lru_test_sample_size; | ||||
|     int cluster_mode; | ||||
|     int cluster_reissue_command; | ||||
|     int cluster_send_asking; | ||||
|     int slave_mode; | ||||
|     int pipe_mode; | ||||
|     int pipe_timeout; | ||||
|  | @ -924,6 +925,29 @@ static int cliConnect(int flags) { | |||
|     return REDIS_OK; | ||||
| } | ||||
| 
 | ||||
| /* In cluster, if server replies ASK, we will redirect to a different node.
 | ||||
|  * Before sending the real command, we need to send ASKING command first. */ | ||||
| static int cliSendAsking() { | ||||
|     redisReply *reply; | ||||
| 
 | ||||
|     config.cluster_send_asking = 0; | ||||
|     if (context == NULL) { | ||||
|         return REDIS_ERR; | ||||
|     } | ||||
|     reply = redisCommand(context,"ASKING"); | ||||
|     if (reply == NULL) { | ||||
|         fprintf(stderr, "\nI/O error\n"); | ||||
|         return REDIS_ERR; | ||||
|     } | ||||
|     int result = REDIS_OK; | ||||
|     if (reply->type == REDIS_REPLY_ERROR) { | ||||
|         result = REDIS_ERR; | ||||
|         fprintf(stderr,"ASKING failed: %s\n",reply->str); | ||||
|     } | ||||
|     freeReplyObject(reply); | ||||
|     return result; | ||||
| } | ||||
| 
 | ||||
| static void cliPrintContextError(void) { | ||||
|     if (context == NULL) return; | ||||
|     fprintf(stderr,"Error: %s\n",context->errstr); | ||||
|  | @ -1299,7 +1323,7 @@ static int cliReadReply(int output_raw_strings) { | |||
|     /* Check if we need to connect to a different node and reissue the
 | ||||
|      * request. */ | ||||
|     if (config.cluster_mode && reply->type == REDIS_REPLY_ERROR && | ||||
|         (!strncmp(reply->str,"MOVED",5) || !strcmp(reply->str,"ASK"))) | ||||
|         (!strncmp(reply->str,"MOVED ",6) || !strncmp(reply->str,"ASK ",4))) | ||||
|     { | ||||
|         char *p = reply->str, *s; | ||||
|         int slot; | ||||
|  | @ -1323,6 +1347,9 @@ static int cliReadReply(int output_raw_strings) { | |||
|             printf("-> Redirected to slot [%d] located at %s:%d\n", | ||||
|                 slot, config.hostip, config.hostport); | ||||
|         config.cluster_reissue_command = 1; | ||||
|         if (!strncmp(reply->str,"ASK ",4)) { | ||||
|             config.cluster_send_asking = 1; | ||||
|         } | ||||
|         cliRefreshPrompt(); | ||||
|     } else if (!config.interactive && config.set_errcode &&  | ||||
|         reply->type == REDIS_REPLY_ERROR)  | ||||
|  | @ -1804,6 +1831,11 @@ static int parseOptions(int argc, char **argv) { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     if (config.hostsocket && config.cluster_mode) { | ||||
|         fprintf(stderr,"Options -c and -s are mutually exclusive.\n"); | ||||
|         exit(1); | ||||
|     } | ||||
| 
 | ||||
|     /* --ldb requires --eval. */ | ||||
|     if (config.eval_ldb && config.eval == NULL) { | ||||
|         fprintf(stderr,"Options --ldb and --ldb-sync-mode require --eval.\n"); | ||||
|  | @ -2021,26 +2053,32 @@ static sds *getSdsArrayFromArgv(int argc, char **argv, int quoted) { | |||
| 
 | ||||
| static int issueCommandRepeat(int argc, char **argv, long repeat) { | ||||
|     while (1) { | ||||
|         if (config.cluster_reissue_command || context == NULL || | ||||
|             context->err == REDIS_ERR_IO || context->err == REDIS_ERR_EOF) | ||||
|         { | ||||
|             if (cliConnect(CC_FORCE) != REDIS_OK) { | ||||
|                 cliPrintContextError(); | ||||
|                 config.cluster_reissue_command = 0; | ||||
|         if (cliSendCommand(argc,argv,repeat) != REDIS_OK) { | ||||
|             cliConnect(CC_FORCE); | ||||
| 
 | ||||
|             /* If we still cannot send the command print error.
 | ||||
|              * We'll try to reconnect the next time. */ | ||||
|             if (cliSendCommand(argc,argv,repeat) != REDIS_OK) { | ||||
|                 return REDIS_ERR; | ||||
|             } | ||||
|         } | ||||
|         config.cluster_reissue_command = 0; | ||||
|         if (config.cluster_send_asking) { | ||||
|             if (cliSendAsking() != REDIS_OK) { | ||||
|                 cliPrintContextError(); | ||||
|                 return REDIS_ERR; | ||||
|             } | ||||
|         } | ||||
|         if (cliSendCommand(argc,argv,repeat) != REDIS_OK) { | ||||
|             cliPrintContextError(); | ||||
|             return REDIS_ERR; | ||||
|         } | ||||
| 
 | ||||
|         /* Issue the command again if we got redirected in cluster mode */ | ||||
|         if (config.cluster_mode && config.cluster_reissue_command) { | ||||
|             /* If cliConnect fails, sleep for a while and try again. */ | ||||
|             if (cliConnect(CC_FORCE) != REDIS_OK) | ||||
|                 sleep(1); | ||||
|         } else { | ||||
|             break; | ||||
|             continue; | ||||
|         } | ||||
|         break; | ||||
|     } | ||||
|     return REDIS_OK; | ||||
| } | ||||
|  | @ -8248,6 +8286,7 @@ int main(int argc, char **argv) { | |||
|     config.lru_test_mode = 0; | ||||
|     config.lru_test_sample_size = 0; | ||||
|     config.cluster_mode = 0; | ||||
|     config.cluster_send_asking = 0; | ||||
|     config.slave_mode = 0; | ||||
|     config.getrdb_mode = 0; | ||||
|     config.stat_mode = 0; | ||||
|  |  | |||
|  | @ -0,0 +1,58 @@ | |||
| # A fake Redis node for replaying predefined/expected traffic with a client. | ||||
| # | ||||
| # Usage: tclsh fake_redis_node.tcl PORT COMMAND REPLY [ COMMAND REPLY [ ... ] ] | ||||
| # | ||||
| # Commands are given as space-separated strings, e.g. "GET foo", and replies as | ||||
| # RESP-encoded replies minus the trailing \r\n, e.g. "+OK". | ||||
| 
 | ||||
| set port [lindex $argv 0]; | ||||
| set expected_traffic [lrange $argv 1 end]; | ||||
| 
 | ||||
| # Reads and parses a command from a socket and returns it as a space-separated | ||||
| # string, e.g. "set foo bar". | ||||
| proc read_command {sock} { | ||||
|     set char [read $sock 1] | ||||
|     switch $char { | ||||
|         * { | ||||
|             set numargs [gets $sock] | ||||
|             set result {} | ||||
|             for {set i 0} {$i<$numargs} {incr i} { | ||||
|                 read $sock 1;       # dollar sign | ||||
|                 set len [gets $sock] | ||||
|                 set str [read $sock $len] | ||||
|                 gets $sock;         # trailing \r\n | ||||
|                 lappend result $str | ||||
|             } | ||||
|             return $result | ||||
|         } | ||||
|         {} { | ||||
|             # EOF | ||||
|             return {} | ||||
|         } | ||||
|         default { | ||||
|             # Non-RESP command | ||||
|             set rest [gets $sock] | ||||
|             return "$char$rest" | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| proc accept {sock host port} { | ||||
|     global expected_traffic | ||||
|     foreach {expect_cmd reply} $expected_traffic { | ||||
|         if {[eof $sock]} {break} | ||||
|         set cmd [read_command $sock] | ||||
|         if {[string equal -nocase $cmd $expect_cmd]} { | ||||
|             puts $sock $reply | ||||
|             flush $sock | ||||
|         } else { | ||||
|             puts $sock "-ERR unexpected command $cmd" | ||||
|             break | ||||
|         } | ||||
|     } | ||||
|     close $sock | ||||
| } | ||||
| 
 | ||||
| socket -server accept $port | ||||
| after 5000 set done timeout | ||||
| vwait done | ||||
|  | @ -64,8 +64,8 @@ start_server {tags {"cli"}} { | |||
|         set _ $tmp | ||||
|     } | ||||
| 
 | ||||
|     proc _run_cli {opts args} { | ||||
|         set cmd [rediscli [srv host] [srv port] [list -n 9 {*}$args]] | ||||
|     proc _run_cli {host port db opts args} { | ||||
|         set cmd [rediscli $host $port [list -n $db {*}$args]] | ||||
|         foreach {key value} $opts { | ||||
|             if {$key eq "pipe"} { | ||||
|                 set cmd "sh -c \"$value | $cmd\"" | ||||
|  | @ -84,15 +84,19 @@ start_server {tags {"cli"}} { | |||
|     } | ||||
| 
 | ||||
|     proc run_cli {args} { | ||||
|         _run_cli {} {*}$args | ||||
|         _run_cli [srv host] [srv port] 9 {} {*}$args | ||||
|     } | ||||
| 
 | ||||
|     proc run_cli_with_input_pipe {cmd args} { | ||||
|         _run_cli [list pipe $cmd] -x {*}$args | ||||
|         _run_cli [srv host] [srv port] 9 [list pipe $cmd] -x {*}$args | ||||
|     } | ||||
| 
 | ||||
|     proc run_cli_with_input_file {path args} { | ||||
|         _run_cli [list path $path] -x {*}$args | ||||
|         _run_cli [srv host] [srv port] 9 [list path $path] -x {*}$args | ||||
|     } | ||||
| 
 | ||||
|     proc run_cli_host_port_db {host port db args} { | ||||
|         _run_cli $host $port $db {} {*}$args | ||||
|     } | ||||
| 
 | ||||
|     proc test_nontty_cli {name code} { | ||||
|  | @ -207,6 +211,23 @@ start_server {tags {"cli"}} { | |||
|         assert_equal "foo\nbar" [run_cli lrange list 0 -1] | ||||
|     } | ||||
| 
 | ||||
|     test_nontty_cli "ASK redirect test" { | ||||
|         # Set up two fake Redis nodes. | ||||
|         set tclsh [info nameofexecutable] | ||||
|         set script "tests/helpers/fake_redis_node.tcl" | ||||
|         set port1 [find_available_port $::baseport $::portcount] | ||||
|         set port2 [find_available_port $::baseport $::portcount] | ||||
|         set p1 [exec $tclsh $script $port1 \ | ||||
|                 "SET foo bar" "-ASK 12182 127.0.0.1:$port2" &] | ||||
|         set p2 [exec $tclsh $script $port2 \ | ||||
|                 "ASKING" "+OK" \ | ||||
|                 "SET foo bar" "+OK" &] | ||||
|         # Sleep to make sure both fake nodes have started listening | ||||
|         after 100 | ||||
|         # Run the cli | ||||
|         assert_equal "OK" [run_cli_host_port_db "127.0.0.1" $port1 0 -c SET foo bar] | ||||
|     } | ||||
| 
 | ||||
|     test_nontty_cli "Quoted input arguments" { | ||||
|         r set "\x00\x00" "value" | ||||
|         assert_equal "value" [run_cli --quoted-input get {"\x00\x00"}] | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue