2014-04-24 11:08:22 +02: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 14:25:48 -04:00
# This software is released under the BSD License. See the COPYING file for
2014-02-17 17:37:56 +01:00
# more information.
package require Tcl 8.5
set tcl_precision 17
2014-04-24 11:08:22 +02:00
source ../ support/ redis.tcl
source ../ support/ util.tcl
source ../ support/ server.tcl
source ../ support/ test.tcl
2014-02-17 17:37:56 +01:00
set : : verbose 0
2015-01-09 17:43:48 -05:00
set : : valgrind 0
2019-09-12 10:56:54 +03:00
set : : tls 0
2014-02-23 17:57:53 +01:00
set : : pause_on_error 0
2014-03-04 12:05:49 +01:00
set : : simulate_error 0
2015-03-30 14:29:01 +02:00
set : : failed 0
2014-02-17 17:37:56 +01:00
set : : sentinel_instances { }
set : : redis_instances { }
set : : sentinel_base_port 20000
set : : redis_base_port 30000
2020-05-26 11:00:48 +03:00
set : : redis_port_count 1024
2014-02-17 17:37:56 +01:00
set : : pids { } ; # We kill everything at exit
set : : dirs { } ; # We remove all the temp dirs at exit
2014-02-20 16:28:38 +01:00
set : : run_matching { } ; # If non empty, only tests matching pattern are run.
2014-02-17 17:37:56 +01:00
2014-04-24 11:08:22 +02: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 17:37:56 +01: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
}
2015-01-22 18:57:45 +01:00
# Execute the specified instance of the server specified by 'type', using
# the provided configuration file. Returns the PID of the process.
proc exec_instance { type cfgfile} {
if { $type eq " r e d i s " } {
set prgname redis-server
} elseif { $type eq " s e n t i n e l " } {
set prgname redis-sentinel
} else {
error " U n k n o w n i n s t a n c e t y p e . "
}
if { $::valgrind } {
set pid [ exec valgrind - - track-origins= yes - - suppressions= ../ ../ ../ src/ valgrind.sup - - show-reachable= no - - show-possibly-lost= no - - leak-check= full ../ ../ ../ src/ $ { prgname } $cfgfile & ]
} else {
set pid [ exec ../ ../ ../ src/ $ { prgname } $cfgfile & ]
}
return $pid
}
2014-02-17 17:37:56 +01:00
# Spawn a redis or sentinel instance, depending on 'type'.
2014-04-24 10:50:51 +02:00
proc spawn_instance { type base_port count { conf { } } } {
2014-02-17 17:37:56 +01:00
for { set j 0 } { $j < $count } { incr j} {
2020-05-26 11:00:48 +03:00
set port [ find_available_port $base_port $::redis_port_count ]
2014-02-17 17:37:56 +01:00
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 10:50:51 +02:00
# Create a directory for this instance.
2014-02-17 17:37:56 +01:00
set dirname " $ { t y p e } _ $ { j } "
lappend : : dirs $dirname
catch { exec rm - rf $dirname }
file mkdir $dirname
2014-04-24 10:50:51 +02:00
# Write the instance config file.
2014-02-17 17:37:56 +01:00
set cfgfile [ file join $dirname $type.conf ]
set cfg [ open $cfgfile w]
2019-09-12 10:56:54 +03:00
if { $::tls } {
puts $cfg " t l s - p o r t $ p o r t "
puts $cfg " t l s - r e p l i c a t i o n y e s "
puts $cfg " t l s - c l u s t e r y e s "
puts $cfg " p o r t 0 "
puts $cfg [ format " t l s - c e r t - f i l e % s / . . / . . / t l s / r e d i s . c r t " [ pwd ] ]
puts $cfg [ format " t l s - k e y - f i l e % s / . . / . . / t l s / r e d i s . k e y " [ pwd ] ]
puts $cfg [ format " t l s - d h - p a r a m s - f i l e % s / . . / . . / t l s / r e d i s . d h " [ pwd ] ]
puts $cfg [ format " t l s - c a - c e r t - f i l e % s / . . / . . / t l s / c a . c r t " [ pwd ] ]
puts $cfg " l o g l e v e l d e b u g "
} else {
puts $cfg " p o r t $ p o r t "
}
2014-02-17 17:37:56 +01:00
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 10:50:51 +02:00
# Add additional config files
foreach directive $conf {
puts $cfg $directive
}
2014-02-17 17:37:56 +01:00
close $cfg
# Finally exec it and remember the pid for later cleanup.
2015-01-22 18:57:45 +01:00
set pid [ exec_instance $type $cfgfile ]
2014-02-22 17:26:30 +01:00
lappend : : pids $pid
2014-02-17 17:37:56 +01: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
2019-09-12 10:56:54 +03:00
set link [ redis 127.0 .0.1 $port 0 $::tls ]
2014-06-18 15:54:55 +02:00
$link reconnect 1
2014-02-18 11:04:01 +01:00
lappend : : $ { type } _instances [ list \
2014-02-22 17:26:30 +01:00
pid $pid \
2014-02-17 17:37:56 +01:00
host 127.0 .0.1 \
port $port \
2014-06-18 15:54:55 +02:00
link $link \
2014-02-17 17:37:56 +01:00
]
}
}
2016-01-15 10:19:28 +01:00
proc log_crashes { } {
set start_pattern { * REDIS BUG REPORT START* }
set logs [ glob * / log.txt]
foreach log $logs {
set fd [ open $log ]
set found 0
while { [ gets $fd line] >= 0 } {
if { [ string match $start_pattern $line ] } {
puts " \n * * * C r a s h r e p o r t f o u n d i n $ l o g * * * "
set found 1
}
if { $found } { puts $line }
}
}
}
2014-02-17 17:37:56 +01:00
proc cleanup { } {
puts " C l e a n i n g u p . . . "
2016-01-15 10:19:28 +01:00
log_crashes
2014-02-17 17:37:56 +01:00
foreach pid $::pids {
catch { exec kill - 9 $pid }
}
foreach dir $::dirs {
catch { exec rm - rf $dir }
}
}
proc abort_sentinel_test msg {
2016-01-15 10:19:28 +01:00
incr : : failed
2014-02-17 17:37:56 +01:00
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 16:18:34 +01:00
if { $::pause_on_error } pause_on_error
2014-02-17 17:37:56 +01:00
cleanup
exit 1
}
2014-02-20 16:28:38 +01: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 17:57:53 +01:00
} elseif { $opt eq " - - p a u s e - o n - e r r o r " } {
set : : pause_on_error 1
2014-03-04 12:05:49 +01:00
} elseif { $opt eq " - - f a i l " } {
set : : simulate_error 1
2015-01-09 17:43:48 -05:00
} elseif { $opt eq { --valgrind } } {
set : : valgrind 1
2019-09-12 10:56:54 +03:00
} elseif { $opt eq { --tls } } {
package require tls 1.6
: : tls : : init \
-cafile " $ : : t l s d i r / c a . c r t " \
-certfile " $ : : t l s d i r / r e d i s . c r t " \
-keyfile " $ : : t l s d i r / r e d i s . k e y "
set : : tls 1
2014-02-20 16:28:38 +01:00
} elseif { $opt eq " - - h e l p " } {
2014-03-04 11:17:27 +01: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 12:05:49 +01: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 . "
2015-12-29 15:27:26 +01:00
puts " - - v a l g r i n d R u n w i t h v a l g r i n d . "
2014-03-04 11:17:27 +01:00
puts " - - h e l p S h o w s t h i s h e l p . "
2014-02-20 16:28:38 +01: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 17:57:53 +01: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 18:01:30 +01:00
puts " "
2014-02-23 17:57:53 +01: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 15:55:36 +01: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 18:01:30 +01:00
while 1 {
puts - nonewline " > "
2014-02-23 17:57:53 +01:00
flush stdout
2014-03-04 12:05:49 +01:00
set line [ gets stdin]
set argv [ split $line " " ]
set cmd [ lindex $argv 0 ]
if { $cmd eq { continue } } {
break
2014-06-10 15:19:35 +02: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 12:05:49 +01: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 15:55:36 +01: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 15:19:35 +02: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 15:55:36 +01: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 12:05:49 +01:00
} else {
set errcode [ catch { eval $line } retval]
2014-03-04 15:55:36 +01:00
if { $retval ne { } } { puts " $ r e t v a l " }
2014-03-04 12:05:49 +01:00
}
2014-02-23 17:57:53 +01:00
}
}
2014-02-17 17:37:56 +01: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 11:17:27 +01:00
set ts [ clock format [ clock seconds] - format % H:% M:% S]
puts - nonewline " $ t s > $ d e s c r : "
2014-02-17 17:37:56 +01:00
flush stdout
if { [ catch { set retval [ uplevel 1 $code ] } error] } {
2015-03-30 14:29:01 +02:00
incr : : failed
2014-02-17 17:37:56 +01:00
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 17:57:53 +01:00
if { $::pause_on_error } pause_on_error
2014-02-25 08:33:41 +01: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 17:37:56 +01:00
} else {
# Re-raise, let handler up the stack take care of this.
error $error $::errorInfo
}
} else {
puts [ colorstr green OK]
}
}
2016-01-02 13:14:23 +01:00
# Check memory leaks when running on OSX using the "leaks" utility.
proc check_leaks instance_types {
if { [ string match { * Darwin * } [ exec uname - a] ] } {
puts - nonewline " T e s t i n g f o r m e m o r y l e a k s . . . " ; flush stdout
foreach type $instance_types {
foreach_instance_id [ set : : $ { type } _instances] id {
if { [ instance_is_killed $type $id ] } continue
set pid [ get_instance_attrib $type $id pid]
set output { 0 leaks}
catch { exec leaks $pid } output
if { [ string match { * process does not exist* } $output ] ||
[ string match { * cannot examine* } $output ] } {
# In a few tests we kill the server process.
set output " 0 l e a k s "
} else {
puts - nonewline " $ t y p e / $ p i d "
flush stdout
}
if { ! [ string match { * 0 leaks* } $output ] } {
puts [ colorstr red " = = = M E M O R Y L E A K D E T E C T E D = = = " ]
puts " I n s t a n c e t y p e $ t y p e , I D $ i d : "
puts $output
puts " = = = "
incr : : failed
}
}
}
puts " "
}
}
2015-03-30 14:29:01 +02:00
# Execute all the units inside the 'tests' directory.
2014-02-17 17:37:56 +01:00
proc run_tests { } {
2014-04-24 11:08:22 +02:00
set tests [ lsort [ glob ../ tests/ * ] ]
2014-02-17 17:37:56 +01:00
foreach test $tests {
2014-02-20 16:28:38 +01:00
if { $::run_matching ne { } && [ string match $::run_matching $test ] == 0 } {
continue
}
2014-02-20 16:57:51 +01:00
if { [ file isdirectory $test ] } continue
2014-02-18 16:31:52 +01: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 17:37:56 +01:00
source $test
2016-01-02 13:14:23 +01:00
check_leaks { redis sentinel}
2014-02-17 17:37:56 +01:00
}
}
2015-03-30 14:29:01 +02:00
# Print a message and exists with 0 / 1 according to zero or more failures.
proc end_tests { } {
if { $::failed == 0 } {
puts " G O O D ! N o e r r o r s . "
exit 0
} else {
2017-08-12 22:21:03 -07:00
puts " W A R N I N G $ : : f a i l e d t e s t ( s ) f a i l e d . "
2015-03-30 14:29:01 +02:00
exit 1
}
}
2014-02-18 11:04:01 +01: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 11:38:49 +01: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 11:04:01 +01:00
# Iterate over IDs of sentinel or redis instances.
2014-02-18 16:31:52 +01:00
proc foreach_instance_id { instances idvar code} {
2014-02-18 11:04:01 +01:00
upvar 1 $idvar id
2014-02-18 16:31:52 +01: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 13:23:32 +01:00
} elseif { $errcode == 4 } {
continue
2014-03-18 15:06:52 +01:00
} elseif { $errcode == 3 } {
break
2014-02-18 16:31:52 +01:00
} elseif { $errcode != 0 } {
return - code $errcode $result
}
2014-02-18 11:04:01 +01:00
}
}
2014-02-18 16:31:52 +01: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 11:04:01 +01:00
proc foreach_redis_id { idvar code} {
2014-02-18 16:31:52 +01:00
set errcode [ catch { uplevel 1 [ list foreach_instance_id $::redis_instances $idvar $code ] } result]
return - code $errcode $result
2014-02-18 11:04:01 +01:00
}
2014-02-18 11:38:49 +01: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 17:26:30 +01: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 11:38:49 +01: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 17:26:30 +01:00
R $id flushall
2014-02-18 11:38:49 +01: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 11:20:53 +01:00
wait_for_condition 1000 50 {
2014-02-18 11:38:49 +01: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 16:31:52 +01: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 17:26:30 +01: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]
2015-01-21 16:46:51 +01:00
set port [ get_instance_attrib $type $id port]
2014-02-25 08:23:48 +01: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 . "
}
2015-01-21 16:46:51 +01:00
2014-02-22 17:26:30 +01: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 ]
2015-01-21 16:46:51 +01:00
# Wait for the port it was using to be available again, so that's not
# an issue to start a new server ASAP with the same port.
set retry 10
while { [ incr retry - 1 ] } {
set port_is_free [ catch { set s [ socket 127.0 .01 $port ] } ]
if { $port_is_free } break
catch { close $s }
after 1000
}
if { $retry == 0 } {
error " P o r t $ p o r t d o e s n o t r e t u r n a v a i l a b l e a f t e r k i l l i n g i n s t a n c e . "
}
2014-02-22 17:26:30 +01:00
}
2014-03-18 14:58:27 +01: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 11:24:05 +02:00
expr { $pid == -1 }
2014-03-18 14:58:27 +01:00
}
2014-02-22 17:26:30 +01: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.
2015-01-22 18:57:45 +01:00
set pid [ exec_instance $type $cfgfile ]
2014-02-25 08:23:48 +01:00
set_instance_attrib $type $id pid $pid
2014-02-22 17:26:30 +01:00
lappend : : pids $pid
# Check that the instance is running
if { [ server_is_up 127.0 .0.1 $port 100 ] == 0 } {
2014-06-30 11:54:13 +02: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 17:26:30 +01:00
}
# Connect with it with a fresh link
2019-09-12 10:56:54 +03:00
set link [ redis 127.0 .0.1 $port 0 $::tls ]
2014-06-18 15:54:55 +02:00
$link reconnect 1
set_instance_attrib $type $id link $link
2016-01-15 16:50:35 +01:00
# Make sure the instance is not loading the dataset when this
# function returns.
while 1 {
catch { [ $link ping ] } retval
if { [ string match { * LOADING * } $retval ] } {
after 100
continue
} else {
break
}
}
2014-02-22 17:26:30 +01:00
}