xref: /redis-3.2.3/tests/instances.tcl (revision a5bb0a07)
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