1# Multi-instance test framework. 2# This is used in order to test Sentinel and Redis Cluster, and provides 3# basic capabilities for spawning and handling N parallel Redis / Sentinel 4# instances. 5# 6# Copyright (C) 2014 Salvatore Sanfilippo [email protected] 7# This software is released under the BSD License. See the COPYING file for 8# more information. 9 10package require Tcl 8.5 11 12set tcl_precision 17 13source ../support/redis.tcl 14source ../support/util.tcl 15source ../support/server.tcl 16source ../support/test.tcl 17 18set ::verbose 0 19set ::valgrind 0 20set ::pause_on_error 0 21set ::simulate_error 0 22set ::sentinel_instances {} 23set ::redis_instances {} 24set ::sentinel_base_port 20000 25set ::redis_base_port 30000 26set ::pids {} ; # We kill everything at exit 27set ::dirs {} ; # We remove all the temp dirs at exit 28set ::run_matching {} ; # If non empty, only tests matching pattern are run. 29 30if {[catch {cd tmp}]} { 31 puts "tmp directory not found." 32 puts "Please run this test from the Redis source root." 33 exit 1 34} 35 36# Spawn a redis or sentinel instance, depending on 'type'. 37proc spawn_instance {type base_port count {conf {}}} { 38 for {set j 0} {$j < $count} {incr j} { 39 set port [find_available_port $base_port] 40 incr base_port 41 puts "Starting $type #$j at port $port" 42 43 # Create a directory for this instance. 44 set dirname "${type}_${j}" 45 lappend ::dirs $dirname 46 catch {exec rm -rf $dirname} 47 file mkdir $dirname 48 49 # Write the instance config file. 50 set cfgfile [file join $dirname $type.conf] 51 set cfg [open $cfgfile w] 52 puts $cfg "port $port" 53 puts $cfg "dir ./$dirname" 54 puts $cfg "logfile log.txt" 55 # Add additional config files 56 foreach directive $conf { 57 puts $cfg $directive 58 } 59 close $cfg 60 61 # Finally exec it and remember the pid for later cleanup. 62 if {$type eq "redis"} { 63 set prgname redis-server 64 } elseif {$type eq "sentinel"} { 65 set prgname redis-sentinel 66 } else { 67 error "Unknown instance type." 68 } 69 70 if {$::valgrind} { 71 set pid [exec valgrind --track-origins=yes --suppressions=../../../src/valgrind.sup --show-reachable=no --show-possibly-lost=no --leak-check=full ../../../src/${prgname} $cfgfile &] 72 } else { 73 set pid [exec ../../../src/${prgname} $cfgfile &] 74 } 75 76 lappend ::pids $pid 77 78 # Check availability 79 if {[server_is_up 127.0.0.1 $port 100] == 0} { 80 abort_sentinel_test "Problems starting $type #$j: ping timeout" 81 } 82 83 # Push the instance into the right list 84 set link [redis 127.0.0.1 $port] 85 $link reconnect 1 86 lappend ::${type}_instances [list \ 87 pid $pid \ 88 host 127.0.0.1 \ 89 port $port \ 90 link $link \ 91 ] 92 } 93} 94 95proc cleanup {} { 96 puts "Cleaning up..." 97 foreach pid $::pids { 98 catch {exec kill -9 $pid} 99 } 100 foreach dir $::dirs { 101 catch {exec rm -rf $dir} 102 } 103} 104 105proc abort_sentinel_test msg { 106 puts "WARNING: Aborting the test." 107 puts ">>>>>>>> $msg" 108 cleanup 109 exit 1 110} 111 112proc parse_options {} { 113 for {set j 0} {$j < [llength $::argv]} {incr j} { 114 set opt [lindex $::argv $j] 115 set val [lindex $::argv [expr $j+1]] 116 if {$opt eq "--single"} { 117 incr j 118 set ::run_matching "*${val}*" 119 } elseif {$opt eq "--pause-on-error"} { 120 set ::pause_on_error 1 121 } elseif {$opt eq "--fail"} { 122 set ::simulate_error 1 123 } elseif {$opt eq {--valgrind}} { 124 set ::valgrind 1 125 } elseif {$opt eq "--help"} { 126 puts "Hello, I'm sentinel.tcl and I run Sentinel unit tests." 127 puts "\nOptions:" 128 puts "--single <pattern> Only runs tests specified by pattern." 129 puts "--pause-on-error Pause for manual inspection on error." 130 puts "--fail Simulate a test failure." 131 puts "--help Shows this help." 132 exit 0 133 } else { 134 puts "Unknown option $opt" 135 exit 1 136 } 137 } 138} 139 140# If --pause-on-error option was passed at startup this function is called 141# on error in order to give the developer a chance to understand more about 142# the error condition while the instances are still running. 143proc pause_on_error {} { 144 puts "" 145 puts [colorstr yellow "*** Please inspect the error now ***"] 146 puts "\nType \"continue\" to resume the test, \"help\" for help screen.\n" 147 while 1 { 148 puts -nonewline "> " 149 flush stdout 150 set line [gets stdin] 151 set argv [split $line " "] 152 set cmd [lindex $argv 0] 153 if {$cmd eq {continue}} { 154 break 155 } elseif {$cmd eq {show-redis-logs}} { 156 set count 10 157 if {[lindex $argv 1] ne {}} {set count [lindex $argv 1]} 158 foreach_redis_id id { 159 puts "=== REDIS $id ====" 160 puts [exec tail -$count redis_$id/log.txt] 161 puts "---------------------\n" 162 } 163 } elseif {$cmd eq {show-sentinel-logs}} { 164 set count 10 165 if {[lindex $argv 1] ne {}} {set count [lindex $argv 1]} 166 foreach_sentinel_id id { 167 puts "=== SENTINEL $id ====" 168 puts [exec tail -$count sentinel_$id/log.txt] 169 puts "---------------------\n" 170 } 171 } elseif {$cmd eq {ls}} { 172 foreach_redis_id id { 173 puts -nonewline "Redis $id" 174 set errcode [catch { 175 set str {} 176 append str "@[RI $id tcp_port]: " 177 append str "[RI $id role] " 178 if {[RI $id role] eq {slave}} { 179 append str "[RI $id master_host]:[RI $id master_port]" 180 } 181 set str 182 } retval] 183 if {$errcode} { 184 puts " -- $retval" 185 } else { 186 puts $retval 187 } 188 } 189 foreach_sentinel_id id { 190 puts -nonewline "Sentinel $id" 191 set errcode [catch { 192 set str {} 193 append str "@[SI $id tcp_port]: " 194 append str "[join [S $id sentinel get-master-addr-by-name mymaster]]" 195 set str 196 } retval] 197 if {$errcode} { 198 puts " -- $retval" 199 } else { 200 puts $retval 201 } 202 } 203 } elseif {$cmd eq {help}} { 204 puts "ls List Sentinel and Redis instances." 205 puts "show-sentinel-logs \[N\] Show latest N lines of logs." 206 puts "show-redis-logs \[N\] Show latest N lines of logs." 207 puts "S <id> cmd ... arg Call command in Sentinel <id>." 208 puts "R <id> cmd ... arg Call command in Redis <id>." 209 puts "SI <id> <field> Show Sentinel <id> INFO <field>." 210 puts "RI <id> <field> Show Sentinel <id> INFO <field>." 211 puts "continue Resume test." 212 } else { 213 set errcode [catch {eval $line} retval] 214 if {$retval ne {}} {puts "$retval"} 215 } 216 } 217} 218 219# We redefine 'test' as for Sentinel we don't use the server-client 220# architecture for the test, everything is sequential. 221proc test {descr code} { 222 set ts [clock format [clock seconds] -format %H:%M:%S] 223 puts -nonewline "$ts> $descr: " 224 flush stdout 225 226 if {[catch {set retval [uplevel 1 $code]} error]} { 227 if {[string match "assertion:*" $error]} { 228 set msg [string range $error 10 end] 229 puts [colorstr red $msg] 230 if {$::pause_on_error} pause_on_error 231 puts "(Jumping to next unit after error)" 232 return -code continue 233 } else { 234 # Re-raise, let handler up the stack take care of this. 235 error $error $::errorInfo 236 } 237 } else { 238 puts [colorstr green OK] 239 } 240} 241 242proc run_tests {} { 243 set tests [lsort [glob ../tests/*]] 244 foreach test $tests { 245 if {$::run_matching ne {} && [string match $::run_matching $test] == 0} { 246 continue 247 } 248 if {[file isdirectory $test]} continue 249 puts [colorstr yellow "Testing unit: [lindex [file split $test] end]"] 250 source $test 251 } 252} 253 254# The "S" command is used to interact with the N-th Sentinel. 255# The general form is: 256# 257# S <sentinel-id> command arg arg arg ... 258# 259# Example to ping the Sentinel 0 (first instance): S 0 PING 260proc S {n args} { 261 set s [lindex $::sentinel_instances $n] 262 [dict get $s link] {*}$args 263} 264 265# Like R but to chat with Redis instances. 266proc R {n args} { 267 set r [lindex $::redis_instances $n] 268 [dict get $r link] {*}$args 269} 270 271proc get_info_field {info field} { 272 set fl [string length $field] 273 append field : 274 foreach line [split $info "\n"] { 275 set line [string trim $line "\r\n "] 276 if {[string range $line 0 $fl] eq $field} { 277 return [string range $line [expr {$fl+1}] end] 278 } 279 } 280 return {} 281} 282 283proc SI {n field} { 284 get_info_field [S $n info] $field 285} 286 287proc RI {n field} { 288 get_info_field [R $n info] $field 289} 290 291# Iterate over IDs of sentinel or redis instances. 292proc foreach_instance_id {instances idvar code} { 293 upvar 1 $idvar id 294 for {set id 0} {$id < [llength $instances]} {incr id} { 295 set errcode [catch {uplevel 1 $code} result] 296 if {$errcode == 1} { 297 error $result $::errorInfo $::errorCode 298 } elseif {$errcode == 4} { 299 continue 300 } elseif {$errcode == 3} { 301 break 302 } elseif {$errcode != 0} { 303 return -code $errcode $result 304 } 305 } 306} 307 308proc foreach_sentinel_id {idvar code} { 309 set errcode [catch {uplevel 1 [list foreach_instance_id $::sentinel_instances $idvar $code]} result] 310 return -code $errcode $result 311} 312 313proc foreach_redis_id {idvar code} { 314 set errcode [catch {uplevel 1 [list foreach_instance_id $::redis_instances $idvar $code]} result] 315 return -code $errcode $result 316} 317 318# Get the specific attribute of the specified instance type, id. 319proc get_instance_attrib {type id attrib} { 320 dict get [lindex [set ::${type}_instances] $id] $attrib 321} 322 323# Set the specific attribute of the specified instance type, id. 324proc set_instance_attrib {type id attrib newval} { 325 set d [lindex [set ::${type}_instances] $id] 326 dict set d $attrib $newval 327 lset ::${type}_instances $id $d 328} 329 330# Create a master-slave cluster of the given number of total instances. 331# The first instance "0" is the master, all others are configured as 332# slaves. 333proc create_redis_master_slave_cluster n { 334 foreach_redis_id id { 335 if {$id == 0} { 336 # Our master. 337 R $id slaveof no one 338 R $id flushall 339 } elseif {$id < $n} { 340 R $id slaveof [get_instance_attrib redis 0 host] \ 341 [get_instance_attrib redis 0 port] 342 } else { 343 # Instances not part of the cluster. 344 R $id slaveof no one 345 } 346 } 347 # Wait for all the slaves to sync. 348 wait_for_condition 1000 50 { 349 [RI 0 connected_slaves] == ($n-1) 350 } else { 351 fail "Unable to create a master-slaves cluster." 352 } 353} 354 355proc get_instance_id_by_port {type port} { 356 foreach_${type}_id id { 357 if {[get_instance_attrib $type $id port] == $port} { 358 return $id 359 } 360 } 361 fail "Instance $type port $port not found." 362} 363 364# Kill an instance of the specified type/id with SIGKILL. 365# This function will mark the instance PID as -1 to remember that this instance 366# is no longer running and will remove its PID from the list of pids that 367# we kill at cleanup. 368# 369# The instance can be restarted with restart-instance. 370proc kill_instance {type id} { 371 set pid [get_instance_attrib $type $id pid] 372 if {$pid == -1} { 373 error "You tried to kill $type $id twice." 374 } 375 exec kill -9 $pid 376 set_instance_attrib $type $id pid -1 377 set_instance_attrib $type $id link you_tried_to_talk_with_killed_instance 378 379 # Remove the PID from the list of pids to kill at exit. 380 set ::pids [lsearch -all -inline -not -exact $::pids $pid] 381} 382 383# Return true of the instance of the specified type/id is killed. 384proc instance_is_killed {type id} { 385 set pid [get_instance_attrib $type $id pid] 386 expr {$pid == -1} 387} 388 389# Restart an instance previously killed by kill_instance 390proc restart_instance {type id} { 391 set dirname "${type}_${id}" 392 set cfgfile [file join $dirname $type.conf] 393 set port [get_instance_attrib $type $id port] 394 395 # Execute the instance with its old setup and append the new pid 396 # file for cleanup. 397 if {$type eq "redis"} { 398 set prgname redis-server 399 } else { 400 set prgname redis-sentinel 401 } 402 403 if {$::valgrind} { 404 set pid [exec valgrind --track-origins=yes --suppressions=../../../src/valgrind.sup --show-reachable=no --show-possibly-lost=no --leak-check=full ../../../src/${prgname} $cfgfile &] 405 } else { 406 set pid [exec ../../../src/${prgname} $cfgfile &] 407 } 408 409 set_instance_attrib $type $id pid $pid 410 lappend ::pids $pid 411 412 # Check that the instance is running 413 if {[server_is_up 127.0.0.1 $port 100] == 0} { 414 abort_sentinel_test "Problems starting $type #$id: ping timeout" 415 } 416 417 # Connect with it with a fresh link 418 set link [redis 127.0.0.1 $port] 419 $link reconnect 1 420 set_instance_attrib $type $id link $link 421} 422 423