2014-04-24 13:08:22 +04:00
# Multi-instance test framework.
# This is used in order to test Sentinel and Redis Cluster, and provides
# basic capabilities for spawning and handling N parallel Redis / Sentinel
# instances.
#
# Copyright (C) 2014 Salvatore Sanfilippo antirez@gmail.com
2014-07-31 22:25:48 +04:00
# This software is released under the BSD License. See the COPYING file for
2014-02-17 20:37:56 +04:00
# more information.
package require Tcl 8.5
set tcl_precision 17
2014-04-24 13:08:22 +04:00
source ../ support/ redis.tcl
source ../ support/ util.tcl
source ../ support/ server.tcl
source ../ support/ test.tcl
2014-02-17 20:37:56 +04:00
set : : verbose 0
2015-01-10 01:43:48 +03:00
set : : valgrind 0
2014-02-23 20:57:53 +04:00
set : : pause_on_error 0
2014-03-04 15:05:49 +04:00
set : : simulate_error 0
2014-02-17 20:37:56 +04:00
set : : sentinel_instances { }
set : : redis_instances { }
set : : sentinel_base_port 20000
set : : redis_base_port 30000
set : : pids { } ; # We kill everything at exit
set : : dirs { } ; # We remove all the temp dirs at exit
2014-02-20 19:28:38 +04:00
set : : run_matching { } ; # If non empty, only tests matching pattern are run.
2014-02-17 20:37:56 +04:00
2014-04-24 13:08:22 +04:00
if { [ catch { cd tmp} ] } {
puts " t m p d i r e c t o r y n o t f o u n d . "
2014-02-17 20:37:56 +04:00
puts " P l e a s e r u n t h i s t e s t f r o m t h e R e d i s s o u r c e r o o t . "
exit 1
}
# Spawn a redis or sentinel instance, depending on 'type'.
2014-04-24 12:50:51 +04:00
proc spawn_instance { type base_port count { conf { } } } {
2014-02-17 20:37:56 +04:00
for { set j 0 } { $j < $count } { incr j} {
set port [ find_available_port $base_port ]
incr base_port
puts " S t a r t i n g $ t y p e # $ j a t p o r t $ p o r t "
2014-04-24 12:50:51 +04:00
# Create a directory for this instance.
2014-02-17 20:37:56 +04:00
set dirname " $ { t y p e } _ $ { j } "
lappend : : dirs $dirname
catch { exec rm - rf $dirname }
file mkdir $dirname
2014-04-24 12:50:51 +04:00
# Write the instance config file.
2014-02-17 20:37:56 +04:00
set cfgfile [ file join $dirname $type.conf ]
set cfg [ open $cfgfile w]
puts $cfg " p o r t $ p o r t "
puts $cfg " d i r . / $ d i r n a m e "
puts $cfg " l o g f i l e l o g . t x t "
2014-04-24 12:50:51 +04:00
# Add additional config files
foreach directive $conf {
puts $cfg $directive
}
2014-02-17 20:37:56 +04:00
close $cfg
# Finally exec it and remember the pid for later cleanup.
2014-02-18 14:38:49 +04:00
if { $type eq " r e d i s " } {
set prgname redis-server
2014-04-24 12:50:51 +04:00
} elseif { $type eq " s e n t i n e l " } {
2014-02-18 14:38:49 +04:00
set prgname redis-sentinel
2014-04-24 12:50:51 +04:00
} else {
error " U n k n o w n i n s t a n c e t y p e . "
2014-02-18 14:38:49 +04:00
}
2015-01-10 01:43:48 +03:00
if { $::valgrind } {
2015-01-13 19:15:30 +03:00
set pid [ exec valgrind - - track-origins= yes - - suppressions= ../ ../ ../ src/ valgrind.sup - - show-reachable= no - - show-possibly-lost= no - - leak-check= full ../ ../ ../ src/ $ { prgname } $cfgfile & ]
2015-01-10 01:43:48 +03:00
} else {
set pid [ exec ../ ../ ../ src/ $ { prgname } $cfgfile & ]
}
2014-02-22 20:26:30 +04:00
lappend : : pids $pid
2014-02-17 20:37:56 +04:00
# Check availability
if { [ server_is_up 127.0 .0.1 $port 100 ] == 0 } {
abort_sentinel_test " P r o b l e m s s t a r t i n g $ t y p e # $ j : p i n g t i m e o u t "
}
# Push the instance into the right list
2014-06-18 17:54:55 +04:00
set link [ redis 127.0 .0.1 $port ]
$link reconnect 1
2014-02-18 14:04:01 +04:00
lappend : : $ { type } _instances [ list \
2014-02-22 20:26:30 +04:00
pid $pid \
2014-02-17 20:37:56 +04:00
host 127.0 .0.1 \
port $port \
2014-06-18 17:54:55 +04:00
link $link \
2014-02-17 20:37:56 +04:00
]
}
}
proc cleanup { } {
puts " C l e a n i n g u p . . . "
foreach pid $::pids {
catch { exec kill - 9 $pid }
}
foreach dir $::dirs {
catch { exec rm - rf $dir }
}
}
proc abort_sentinel_test msg {
puts " W A R N I N G : A b o r t i n g t h e t e s t . "
puts " > > > > > > > > $ m s g "
2015-01-21 18:18:34 +03:00
if { $::pause_on_error } pause_on_error
2014-02-17 20:37:56 +04:00
cleanup
exit 1
}
2014-02-20 19:28:38 +04:00
proc parse_options { } {
for { set j 0 } { $j < [ llength $::argv ] } { incr j} {
set opt [ lindex $::argv $j ]
set val [ lindex $::argv [ expr $j + 1 ] ]
if { $opt eq " - - s i n g l e " } {
incr j
set : : run_matching " * $ { v a l } * "
2014-02-23 20:57:53 +04:00
} elseif { $opt eq " - - p a u s e - o n - e r r o r " } {
set : : pause_on_error 1
2014-03-04 15:05:49 +04:00
} elseif { $opt eq " - - f a i l " } {
set : : simulate_error 1
2015-01-10 01:43:48 +03:00
} elseif { $opt eq { --valgrind } } {
set : : valgrind 1
2014-02-20 19:28:38 +04:00
} elseif { $opt eq " - - h e l p " } {
puts " H e l l o , I ' m s e n t i n e l . t c l a n d I r u n S e n t i n e l u n i t t e s t s . "
puts " \n O p t i o n s : "
2014-03-04 14:17:27 +04:00
puts " - - s i n g l e < p a t t e r n > O n l y r u n s t e s t s s p e c i f i e d b y p a t t e r n . "
puts " - - p a u s e - o n - e r r o r P a u s e f o r m a n u a l i n s p e c t i o n o n e r r o r . "
2014-03-04 15:05:49 +04:00
puts " - - f a i l S i m u l a t e a t e s t f a i l u r e . "
2014-03-04 14:17:27 +04:00
puts " - - h e l p S h o w s t h i s h e l p . "
2014-02-20 19:28:38 +04:00
exit 0
} else {
puts " U n k n o w n o p t i o n $ o p t "
exit 1
}
}
}
2014-02-23 20:57:53 +04:00
# If --pause-on-error option was passed at startup this function is called
# on error in order to give the developer a chance to understand more about
# the error condition while the instances are still running.
proc pause_on_error { } {
2014-02-23 21:01:30 +04:00
puts " "
2014-02-23 20:57:53 +04:00
puts [ colorstr yellow " * * * P l e a s e i n s p e c t t h e e r r o r n o w * * * " ]
2014-03-04 18:55:36 +04:00
puts " \n T y p e \" c o n t i n u e \" t o r e s u m e t h e t e s t , \" h e l p \" f o r h e l p s c r e e n . \n "
2014-02-23 21:01:30 +04:00
while 1 {
puts - nonewline " > "
2014-02-23 20:57:53 +04:00
flush stdout
2014-03-04 15:05:49 +04:00
set line [ gets stdin]
set argv [ split $line " " ]
set cmd [ lindex $argv 0 ]
if { $cmd eq { continue } } {
break
2014-06-10 17:19:35 +04:00
} elseif { $cmd eq { show-redis-logs } } {
set count 10
if { [ lindex $argv 1 ] ne { } } { set count [ lindex $argv 1 ] }
foreach_redis_id id {
puts " = = = R E D I S $ i d = = = = "
puts [ exec tail - $count redis_$id / log.txt]
puts " - - - - - - - - - - - - - - - - - - - - - \n "
}
2014-03-04 15:05:49 +04:00
} elseif { $cmd eq { show-sentinel-logs } } {
set count 10
if { [ lindex $argv 1 ] ne { } } { set count [ lindex $argv 1 ] }
foreach_sentinel_id id {
puts " = = = S E N T I N E L $ i d = = = = "
puts [ exec tail - $count sentinel_$id / log.txt]
puts " - - - - - - - - - - - - - - - - - - - - - \n "
}
2014-03-04 18:55:36 +04:00
} elseif { $cmd eq { ls } } {
foreach_redis_id id {
puts - nonewline " R e d i s $ i d "
set errcode [ catch {
set str { }
append str " @ [ R I $ i d t c p _ p o r t ] : "
append str " [ R I $ i d r o l e ] "
if { [ RI $id role] eq { slave } } {
append str " [ R I $ i d m a s t e r _ h o s t ] : [ R I $ i d m a s t e r _ p o r t ] "
}
set str
} retval ]
if { $errcode } {
puts " - - $ r e t v a l "
} else {
puts $retval
}
}
foreach_sentinel_id id {
puts - nonewline " S e n t i n e l $ i d "
set errcode [ catch {
set str { }
append str " @ [ S I $ i d t c p _ p o r t ] : "
append str " [ j o i n [ S $ i d s e n t i n e l g e t - m a s t e r - a d d r - b y - n a m e m y m a s t e r ] ] "
set str
} retval ]
if { $errcode } {
puts " - - $ r e t v a l "
} else {
puts $retval
}
}
} elseif { $cmd eq { help } } {
puts " l s L i s t S e n t i n e l a n d R e d i s i n s t a n c e s . "
puts " s h o w - s e n t i n e l - l o g s \[ N \] S h o w l a t e s t N l i n e s o f l o g s . "
2014-06-10 17:19:35 +04:00
puts " s h o w - r e d i s - l o g s \[ N \] S h o w l a t e s t N l i n e s o f l o g s . "
2014-03-04 18:55:36 +04:00
puts " S < i d > c m d . . . a r g C a l l c o m m a n d i n S e n t i n e l < i d > . "
puts " R < i d > c m d . . . a r g C a l l c o m m a n d i n R e d i s < i d > . "
puts " S I < i d > < f i e l d > S h o w S e n t i n e l < i d > I N F O < f i e l d > . "
puts " R I < i d > < f i e l d > S h o w S e n t i n e l < i d > I N F O < f i e l d > . "
puts " c o n t i n u e R e s u m e t e s t . "
2014-03-04 15:05:49 +04:00
} else {
set errcode [ catch { eval $line } retval]
2014-03-04 18:55:36 +04:00
if { $retval ne { } } { puts " $ r e t v a l " }
2014-03-04 15:05:49 +04:00
}
2014-02-23 20:57:53 +04:00
}
}
2014-02-17 20:37:56 +04:00
# We redefine 'test' as for Sentinel we don't use the server-client
# architecture for the test, everything is sequential.
proc test { descr code} {
2014-03-04 14:17:27 +04:00
set ts [ clock format [ clock seconds] - format % H:% M:% S]
puts - nonewline " $ t s > $ d e s c r : "
2014-02-17 20:37:56 +04:00
flush stdout
if { [ catch { set retval [ uplevel 1 $code ] } error] } {
if { [ string match " a s s e r t i o n : * " $error ] } {
set msg [ string range $error 10 end]
puts [ colorstr red $msg ]
2014-02-23 20:57:53 +04:00
if { $::pause_on_error } pause_on_error
2014-02-25 11:33:41 +04:00
puts " ( J u m p i n g t o n e x t u n i t a f t e r e r r o r ) "
return - code continue
2014-02-17 20:37:56 +04:00
} else {
# Re-raise, let handler up the stack take care of this.
error $error $::errorInfo
}
} else {
puts [ colorstr green OK]
}
}
proc run_tests { } {
2014-04-24 13:08:22 +04:00
set tests [ lsort [ glob ../ tests/ * ] ]
2014-02-17 20:37:56 +04:00
foreach test $tests {
2014-02-20 19:28:38 +04:00
if { $::run_matching ne { } && [ string match $::run_matching $test ] == 0 } {
continue
}
2014-02-20 19:57:51 +04:00
if { [ file isdirectory $test ] } continue
2014-02-18 19:31:52 +04:00
puts [ colorstr yellow " T e s t i n g u n i t : [ l i n d e x [ f i l e s p l i t $ t e s t ] e n d ] " ]
2014-02-17 20:37:56 +04:00
source $test
}
}
2014-02-18 14:04:01 +04:00
# The "S" command is used to interact with the N-th Sentinel.
# The general form is:
#
# S <sentinel-id> command arg arg arg ...
#
# Example to ping the Sentinel 0 (first instance): S 0 PING
proc S { n args} {
set s [ lindex $::sentinel_instances $n ]
[ dict get $s link] { * } $args
}
# Like R but to chat with Redis instances.
proc R { n args} {
set r [ lindex $::redis_instances $n ]
[ dict get $r link] { * } $args
}
2014-02-18 14:38:49 +04:00
proc get_info_field { info field} {
set fl [ string length $field ]
append field :
foreach line [ split $info " \n " ] {
set line [ string trim $line " \r \n " ]
if { [ string range $line 0 $fl ] eq $field } {
return [ string range $line [ expr { $fl + 1 } ] end]
}
}
return { }
}
proc SI { n field} {
get_info_field [ S $n info] $field
}
proc RI { n field} {
get_info_field [ R $n info] $field
}
2014-02-18 14:04:01 +04:00
# Iterate over IDs of sentinel or redis instances.
2014-02-18 19:31:52 +04:00
proc foreach_instance_id { instances idvar code} {
2014-02-18 14:04:01 +04:00
upvar 1 $idvar id
2014-02-18 19:31:52 +04:00
for { set id 0 } { $id < [ llength $instances ] } { incr id} {
set errcode [ catch { uplevel 1 $code } result]
if { $errcode == 1 } {
error $result $::errorInfo $::errorCode
2014-03-03 16:23:32 +04:00
} elseif { $errcode == 4 } {
continue
2014-03-18 18:06:52 +04:00
} elseif { $errcode == 3 } {
break
2014-02-18 19:31:52 +04:00
} elseif { $errcode != 0 } {
return - code $errcode $result
}
2014-02-18 14:04:01 +04:00
}
}
2014-02-18 19:31:52 +04:00
proc foreach_sentinel_id { idvar code} {
set errcode [ catch { uplevel 1 [ list foreach_instance_id $::sentinel_instances $idvar $code ] } result]
return - code $errcode $result
}
2014-02-18 14:04:01 +04:00
proc foreach_redis_id { idvar code} {
2014-02-18 19:31:52 +04:00
set errcode [ catch { uplevel 1 [ list foreach_instance_id $::redis_instances $idvar $code ] } result]
return - code $errcode $result
2014-02-18 14:04:01 +04:00
}
2014-02-18 14:38:49 +04:00
# Get the specific attribute of the specified instance type, id.
proc get_instance_attrib { type id attrib} {
dict get [ lindex [ set : : $ { type } _instances] $id ] $attrib
}
2014-02-22 20:26:30 +04:00
# Set the specific attribute of the specified instance type, id.
proc set_instance_attrib { type id attrib newval} {
set d [ lindex [ set : : $ { type } _instances] $id ]
dict set d $attrib $newval
lset : : $ { type } _instances $id $d
}
2014-02-18 14:38:49 +04:00
# Create a master-slave cluster of the given number of total instances.
# The first instance "0" is the master, all others are configured as
# slaves.
proc create_redis_master_slave_cluster n {
foreach_redis_id id {
if { $id == 0 } {
# Our master.
R $id slaveof no one
2014-02-22 20:26:30 +04:00
R $id flushall
2014-02-18 14:38:49 +04:00
} elseif { $id < $n } {
R $id slaveof [ get_instance_attrib redis 0 host] \
[ get_instance_attrib redis 0 port]
} else {
# Instances not part of the cluster.
R $id slaveof no one
}
}
# Wait for all the slaves to sync.
2014-03-04 14:20:53 +04:00
wait_for_condition 1000 50 {
2014-02-18 14:38:49 +04:00
[ RI 0 connected_slaves] == ( $n-1 )
} else {
fail " U n a b l e t o c r e a t e a m a s t e r - s l a v e s c l u s t e r . "
}
}
2014-02-18 19:31:52 +04:00
proc get_instance_id_by_port { type port} {
foreach_ $ { type } _id id {
if { [ get_instance_attrib $type $id port] == $port } {
return $id
}
}
fail " I n s t a n c e $ t y p e p o r t $ p o r t n o t f o u n d . "
}
2014-02-22 20:26:30 +04:00
# Kill an instance of the specified type/id with SIGKILL.
# This function will mark the instance PID as -1 to remember that this instance
# is no longer running and will remove its PID from the list of pids that
# we kill at cleanup.
#
# The instance can be restarted with restart-instance.
proc kill_instance { type id} {
set pid [ get_instance_attrib $type $id pid]
2014-02-25 11:23:48 +04:00
if { $pid == -1 } {
error " Y o u t r i e d t o k i l l $ t y p e $ i d t w i c e . "
}
2014-02-22 20:26:30 +04:00
exec kill - 9 $pid
set_instance_attrib $type $id pid - 1
set_instance_attrib $type $id link you_tried_to_talk_with_killed_instance
# Remove the PID from the list of pids to kill at exit.
set : : pids [ lsearch - all - inline - not - exact $::pids $pid ]
}
2014-03-18 17:58:27 +04:00
# Return true of the instance of the specified type/id is killed.
proc instance_is_killed { type id} {
set pid [ get_instance_attrib $type $id pid]
2014-05-19 13:24:05 +04:00
expr { $pid == -1 }
2014-03-18 17:58:27 +04:00
}
2014-02-22 20:26:30 +04:00
# Restart an instance previously killed by kill_instance
proc restart_instance { type id} {
set dirname " $ { t y p e } _ $ { i d } "
set cfgfile [ file join $dirname $type.conf ]
set port [ get_instance_attrib $type $id port]
# Execute the instance with its old setup and append the new pid
# file for cleanup.
if { $type eq " r e d i s " } {
set prgname redis-server
} else {
set prgname redis-sentinel
}
2015-01-10 01:43:48 +03:00
if { $::valgrind } {
2015-01-13 19:15:30 +03:00
set pid [ exec valgrind - - track-origins= yes - - suppressions= ../ ../ ../ src/ valgrind.sup - - show-reachable= no - - show-possibly-lost= no - - leak-check= full ../ ../ ../ src/ $ { prgname } $cfgfile & ]
2015-01-10 01:43:48 +03:00
} else {
set pid [ exec ../ ../ ../ src/ $ { prgname } $cfgfile & ]
}
2014-02-25 11:23:48 +04:00
set_instance_attrib $type $id pid $pid
2014-02-22 20:26:30 +04:00
lappend : : pids $pid
# Check that the instance is running
if { [ server_is_up 127.0 .0.1 $port 100 ] == 0 } {
2014-06-30 13:54:13 +04:00
abort_sentinel_test " P r o b l e m s s t a r t i n g $ t y p e # $ i d : p i n g t i m e o u t "
2014-02-22 20:26:30 +04:00
}
# Connect with it with a fresh link
2014-06-18 17:54:55 +04:00
set link [ redis 127.0 .0.1 $port ]
$link reconnect 1
set_instance_attrib $type $id link $link
2014-02-22 20:26:30 +04:00
}