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