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