Skip to content

Commit 381234f

Browse files
committed
Merge pull request #474 from redis/sentinel
Sentinel support
2 parents e6e62f3 + 35a3834 commit 381234f

File tree

7 files changed

+352
-3
lines changed

7 files changed

+352
-3
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Gemfile.lock
77
/.yardoc
88
/coverage/*
99
/doc/
10+
/examples/sentinel/sentinel.conf
1011
/nohup.out
1112
/pkg/*
1213
/rdsrv

README.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,34 @@ available on [rdoc.info][rdoc].
8585

8686
[rdoc]: http://rdoc.info/github/redis/redis-rb/
8787

88+
## Sentinel support
89+
90+
The client is able to perform automatic failovers by using [Redis
91+
Sentinel](http://redis.io/topics/sentinel). Make sure to run Redis 2.8+
92+
if you want to use this feature.
93+
94+
To connect using Sentinel, use:
95+
96+
```ruby
97+
SENTINELS = [{:host => "127.0.0.1", :port => 26380},
98+
{:host => "127.0.0.1", :port => 26381}]
99+
100+
redis = Redis.new(:url => "redis://mymaster", :sentinels => SENTINELS, :role => :master)
101+
```
102+
103+
* The master name identifies a group of Redis instances composed of a master
104+
and one or more slaves (`mymaster` in the example).
105+
106+
* It is possible to optionally provide a role. The allowed roles are `master`
107+
and `slave`. When the role is `slave`, the client will try to connect to a
108+
random slave of the specified master.
109+
110+
* When using the Sentinel support you need to specify a list of sentinels to
111+
connect to. The list does not need to enumerate all your Sentinel instances,
112+
but a few so that if one is down the client will try the next one. The client
113+
is able to remember the last Sentinel that was able to reply correctly and will
114+
use it for the next requests.
115+
88116
## Storing objects
89117

90118
Redis only stores strings as values. If you want to store an object, you
@@ -166,7 +194,7 @@ end
166194
to redis, AND
167195
- your own code prevents the parent process from using the redis
168196
connection while a child is alive
169-
197+
170198
Improper use of `inherit_socket` will result in corrupted and/or incorrect
171199
responses.
172200

examples/consistency.rb

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# This file implements a simple consistency test for Redis-rb (or any other
2+
# Redis environment if you pass a different client object) where a client
3+
# writes to the database using INCR in order to increment keys, but actively
4+
# remember the value the key should have. Before every write a read is performed
5+
# to check if the value in the database matches the value expected.
6+
#
7+
# In this way this program can check for lost writes, or acknowledged writes
8+
# that were executed.
9+
#
10+
# Copyright (C) 2013-2014 Salvatore Sanfilippo <[email protected]>
11+
#
12+
# Permission is hereby granted, free of charge, to any person obtaining
13+
# a copy of this software and associated documentation files (the
14+
# "Software"), to deal in the Software without restriction, including
15+
# without limitation the rights to use, copy, modify, merge, publish,
16+
# distribute, sublicense, and/or sell copies of the Software, and to
17+
# permit persons to whom the Software is furnished to do so, subject to
18+
# the following conditions:
19+
#
20+
# The above copyright notice and this permission notice shall be
21+
# included in all copies or substantial portions of the Software.
22+
#
23+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
24+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
25+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
26+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
27+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
28+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
29+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
30+
31+
require 'redis'
32+
33+
class ConsistencyTester
34+
def initialize(redis)
35+
@r = redis
36+
@working_set = 10000
37+
@keyspace = 100000
38+
@writes = 0
39+
@reads = 0
40+
@failed_writes = 0
41+
@failed_reads = 0
42+
@lost_writes = 0
43+
@not_ack_writes = 0
44+
@delay = 0
45+
@cached = {} # We take our view of data stored in the DB.
46+
@prefix = [Process.pid.to_s,Time.now.usec,@r.object_id,""].join("|")
47+
@errtime = {}
48+
end
49+
50+
def genkey
51+
# Write more often to a small subset of keys
52+
ks = rand() > 0.5 ? @keyspace : @working_set
53+
@prefix+"key_"+rand(ks).to_s
54+
end
55+
56+
def check_consistency(key,value)
57+
expected = @cached[key]
58+
return if !expected # We lack info about previous state.
59+
if expected > value
60+
@lost_writes += expected-value
61+
elsif expected < value
62+
@not_ack_writes += value-expected
63+
end
64+
end
65+
66+
def puterr(msg)
67+
if !@errtime[msg] || Time.now.to_i != @errtime[msg]
68+
puts msg
69+
end
70+
@errtime[msg] = Time.now.to_i
71+
end
72+
73+
def test
74+
last_report = Time.now.to_i
75+
while true
76+
# Read
77+
key = genkey
78+
begin
79+
val = @r.get(key)
80+
check_consistency(key,val.to_i)
81+
@reads += 1
82+
rescue => e
83+
puterr "Reading: #{e.class}: #{e.message} (#{e.backtrace.first})"
84+
@failed_reads += 1
85+
end
86+
87+
# Write
88+
begin
89+
@cached[key] = @r.incr(key).to_i
90+
@writes += 1
91+
rescue => e
92+
puterr "Writing: #{e.class}: #{e.message} (#{e.backtrace.first})"
93+
@failed_writes += 1
94+
end
95+
96+
# Report
97+
sleep @delay
98+
if Time.now.to_i != last_report
99+
report = "#{@reads} R (#{@failed_reads} err) | " +
100+
"#{@writes} W (#{@failed_writes} err) | "
101+
report += "#{@lost_writes} lost | " if @lost_writes > 0
102+
report += "#{@not_ack_writes} noack | " if @not_ack_writes > 0
103+
last_report = Time.now.to_i
104+
puts report
105+
end
106+
end
107+
end
108+
end
109+
110+
Sentinels = [{:host => "127.0.0.1", :port => 26379},
111+
{:host => "127.0.0.1", :port => 26380}]
112+
r = Redis.new(:url => "redis://master1", :sentinels => Sentinels, :role => :master)
113+
tester = ConsistencyTester.new(r)
114+
tester.test

examples/sentinel.rb

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
require 'redis'
2+
3+
# This example creates a master-slave setup with a sentinel, then connects to
4+
# it and sends write commands in a loop.
5+
#
6+
# After 30 seconds, the master dies. You will be able to see how a new master
7+
# is elected and things continue to work as if nothing happened.
8+
#
9+
# To run this example:
10+
#
11+
# $ ruby -I./lib examples/sentinel.rb
12+
#
13+
14+
at_exit do
15+
begin
16+
Process.kill(:INT, $redises)
17+
rescue Errno::ESRCH
18+
end
19+
20+
Process.waitall
21+
end
22+
23+
$redises = spawn("examples/sentinel/start")
24+
25+
Sentinels = [{:host => "127.0.0.1", :port => 26379},
26+
{:host => "127.0.0.1", :port => 26380}]
27+
r = Redis.new(:url => "redis://master1", :sentinels => Sentinels, :role => :master)
28+
29+
# Set keys into a loop.
30+
#
31+
# The example traps errors so that you can actually try to failover while
32+
# running the script to see redis-rb reconfiguring.
33+
(0..1000000).each{|i|
34+
begin
35+
r.set(i,i)
36+
$stdout.write("SET (#{i} times)\n") if i % 100 == 0
37+
rescue => e
38+
$stdout.write("E")
39+
end
40+
sleep(0.01)
41+
}

examples/sentinel/sentinel.conf

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
sentinel monitor master1 127.0.0.1 6380 2
2+
sentinel down-after-milliseconds master1 5000
3+
sentinel failover-timeout master1 15000
4+
sentinel parallel-syncs master1 1
5+
6+
sentinel monitor master2 127.0.0.1 6381 2
7+
sentinel down-after-milliseconds master2 5000
8+
sentinel failover-timeout master2 15000
9+
sentinel parallel-syncs master2 1

examples/sentinel/start

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#! /usr/bin/env ruby
2+
3+
# This is a helper script used together with examples/sentinel.rb
4+
# It runs two Redis masters, two slaves for each of them, and two sentinels.
5+
# After 30 seconds, the first master dies.
6+
#
7+
# You don't need to run this script yourself. Rather, use examples/sentinel.rb.
8+
9+
require "fileutils"
10+
11+
$pids = []
12+
13+
at_exit do
14+
$pids.each do |pid|
15+
begin
16+
Process.kill(:INT, pid)
17+
rescue Errno::ESRCH
18+
end
19+
end
20+
21+
Process.waitall
22+
end
23+
24+
base = File.expand_path(File.dirname(__FILE__))
25+
26+
# Masters
27+
$pids << spawn("redis-server --port 6380 --loglevel warning")
28+
$pids << spawn("redis-server --port 6381 --loglevel warning")
29+
30+
# Slaves of Master 1
31+
$pids << spawn("redis-server --port 63800 --slaveof 127.0.0.1 6380 --loglevel warning")
32+
$pids << spawn("redis-server --port 63801 --slaveof 127.0.0.1 6380 --loglevel warning")
33+
34+
# Slaves of Master 2
35+
$pids << spawn("redis-server --port 63810 --slaveof 127.0.0.1 6381 --loglevel warning")
36+
$pids << spawn("redis-server --port 63811 --slaveof 127.0.0.1 6381 --loglevel warning")
37+
38+
FileUtils.cp(File.join(base, "sentinel.conf"), "tmp/sentinel1.conf")
39+
FileUtils.cp(File.join(base, "sentinel.conf"), "tmp/sentinel2.conf")
40+
41+
# Sentinels
42+
$pids << spawn("redis-server tmp/sentinel1.conf --sentinel --port 26379")
43+
$pids << spawn("redis-server tmp/sentinel2.conf --sentinel --port 26380")
44+
45+
sleep 30
46+
47+
Process.kill(:KILL, $pids[0])
48+
49+
Process.waitall

0 commit comments

Comments
 (0)