redis_master_slave 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +3 -0
- data/LICENSE +20 -0
- data/README.markdown +83 -0
- data/Rakefile +1 -0
- data/lib/redis_master_slave.rb +14 -0
- data/lib/redis_master_slave/client.rb +153 -0
- data/lib/redis_master_slave/read_only.rb +14 -0
- data/lib/redis_master_slave/version.rb +11 -0
- data/spec/redis_master_slave/client_spec.rb +91 -0
- data/spec/spec_helper.rb +11 -0
- data/spec/support/redis.rb +31 -0
- metadata +112 -0
data/CHANGELOG
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) George Ogata
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
## Redis Master Slave
|
2
|
+
|
3
|
+
Redis master-slave client for Ruby.
|
4
|
+
|
5
|
+
Writes are directed to a master Redis server, while reads are distributed
|
6
|
+
round-robin across any number of slaves.
|
7
|
+
|
8
|
+
## Usage
|
9
|
+
|
10
|
+
require 'redis_master_slave'
|
11
|
+
|
12
|
+
client = RedisMasterSlave.new(YAML.load_file('redis.yml'))
|
13
|
+
|
14
|
+
client.set('a', 1) # writes to master
|
15
|
+
client.get('a') # reads from slaves, round-robin
|
16
|
+
client.master.get('a') # reads directly from master
|
17
|
+
client.slaves[0].get('a') # reads directly from first slave
|
18
|
+
|
19
|
+
### Configuration
|
20
|
+
|
21
|
+
The client can be configured in several ways.
|
22
|
+
|
23
|
+
#### Single configuration hash
|
24
|
+
|
25
|
+
Ideal for configuration via YAML file.
|
26
|
+
|
27
|
+
client = RedisMasterSlave.new(YAML.load_file('redis.yml'))
|
28
|
+
|
29
|
+
Example YAML file:
|
30
|
+
|
31
|
+
master:
|
32
|
+
host: localhost
|
33
|
+
port: 6379
|
34
|
+
slaves:
|
35
|
+
- host: localhost
|
36
|
+
port: 6380
|
37
|
+
- host: localhost
|
38
|
+
port: 6381
|
39
|
+
|
40
|
+
#### URL strings
|
41
|
+
|
42
|
+
Specify the host and port for each Redis server as a URL string:
|
43
|
+
|
44
|
+
master_url = "redis://localhost:6379"
|
45
|
+
slave_urls = [
|
46
|
+
"redis://localhost:6380",
|
47
|
+
"redis://localhost:6381",
|
48
|
+
]
|
49
|
+
client = RedisMasterSlave.new(master_urls, slave_urls)
|
50
|
+
|
51
|
+
#### Separate master and slave configurations
|
52
|
+
|
53
|
+
Specify master and slave configurations as separate hashes:
|
54
|
+
|
55
|
+
master_config = {:host => 'localhost', :port => 6379}
|
56
|
+
slave_configs = []
|
57
|
+
{:host => 'localhost', :port => 6380},
|
58
|
+
{:host => 'localhost', :port => 6381},
|
59
|
+
]
|
60
|
+
client = RedisMasterSlave.new(master_config, slave_configs)
|
61
|
+
|
62
|
+
Each configuration hash is passed directly to `Redis.new`.
|
63
|
+
|
64
|
+
#### Redis client objects
|
65
|
+
|
66
|
+
You can also pass your own Redis client objects:
|
67
|
+
|
68
|
+
master = Redis.new(:host => 'localhost', :port => 6379)
|
69
|
+
slave1 = Redis.new(:host => 'localhost', :port => 6380)
|
70
|
+
slave2 = Redis.new(:host => 'localhost', :port => 6381)
|
71
|
+
client = RedisMasterSlave.new(master, slave1, slave2)
|
72
|
+
|
73
|
+
## Contributing
|
74
|
+
|
75
|
+
* [Bug reports.](https://github.com/oggy/redis_master_slave/issues)
|
76
|
+
* [Source.](https://github.com/oggy/redis_master_slave)
|
77
|
+
* Patches: Fork on Github, send pull request.
|
78
|
+
* Please include tests where practical.
|
79
|
+
* Leave the version alone, or bump it in a separate commit.
|
80
|
+
|
81
|
+
## Copyright
|
82
|
+
|
83
|
+
Copyright (c) George Ogata. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'ritual'
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'redis'
|
2
|
+
|
3
|
+
module RedisMasterSlave
|
4
|
+
autoload :Client, 'redis_master_slave/client'
|
5
|
+
autoload :ReadOnly, 'redis_master_slave/read_only'
|
6
|
+
autoload :ReadOnlyError, 'redis_master_slave/read_only'
|
7
|
+
|
8
|
+
#
|
9
|
+
# Create a new client. Same as Client.new.
|
10
|
+
#
|
11
|
+
def self.new(*args, &block)
|
12
|
+
Client.new(*args, &block)
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
module RedisMasterSlave
|
4
|
+
#
|
5
|
+
# Wrapper around a pair of Redis connections, one master and one
|
6
|
+
# slave.
|
7
|
+
#
|
8
|
+
# Read requests are directed to the slave, others are sent to the
|
9
|
+
# master.
|
10
|
+
#
|
11
|
+
class Client
|
12
|
+
#
|
13
|
+
# Create a new client.
|
14
|
+
#
|
15
|
+
# +master+ and +slave+ may be URL strings, Redis client option
|
16
|
+
# hashes, or Redis clients.
|
17
|
+
#
|
18
|
+
def initialize(*args)
|
19
|
+
case args.size
|
20
|
+
when 1
|
21
|
+
config = args.first
|
22
|
+
|
23
|
+
master_config = config['master'] || config[:master]
|
24
|
+
slave_configs = config['slaves'] || config[:slaves]
|
25
|
+
when 2
|
26
|
+
master_config, slave_configs = *args
|
27
|
+
else
|
28
|
+
raise ArgumentError, "wrong number of arguments (#{args.size} for 1..2)"
|
29
|
+
end
|
30
|
+
|
31
|
+
@master = make_client(master_config) or
|
32
|
+
extend ReadOnly
|
33
|
+
@slaves = slave_configs.map{|config| make_client(config)}
|
34
|
+
@index = 0
|
35
|
+
end
|
36
|
+
|
37
|
+
#
|
38
|
+
# The master client.
|
39
|
+
#
|
40
|
+
attr_accessor :master
|
41
|
+
|
42
|
+
#
|
43
|
+
# The slave client.
|
44
|
+
#
|
45
|
+
attr_accessor :slaves
|
46
|
+
|
47
|
+
#
|
48
|
+
# Index of the slave to use for the next read.
|
49
|
+
#
|
50
|
+
attr_accessor :index
|
51
|
+
|
52
|
+
#
|
53
|
+
# Return the next read slave to use.
|
54
|
+
#
|
55
|
+
# Each call returns the following slave in sequence.
|
56
|
+
#
|
57
|
+
def next_slave
|
58
|
+
slave = slaves[index]
|
59
|
+
@index = (index + 1) % slaves.size
|
60
|
+
slave
|
61
|
+
end
|
62
|
+
|
63
|
+
class << self
|
64
|
+
private
|
65
|
+
|
66
|
+
def send_to_slave(command)
|
67
|
+
class_eval <<-EOS
|
68
|
+
def #{command}(*args, &block)
|
69
|
+
next_slave.#{command}(*args, &block)
|
70
|
+
end
|
71
|
+
EOS
|
72
|
+
end
|
73
|
+
|
74
|
+
def send_to_master(command)
|
75
|
+
class_eval <<-EOS
|
76
|
+
def #{command}(*args, &block)
|
77
|
+
writable_master.#{command}(*args, &block)
|
78
|
+
end
|
79
|
+
EOS
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
send_to_slave :dbsize
|
84
|
+
send_to_slave :exists
|
85
|
+
send_to_slave :get
|
86
|
+
send_to_slave :getbit
|
87
|
+
send_to_slave :getrange
|
88
|
+
send_to_slave :hexists
|
89
|
+
send_to_slave :hget
|
90
|
+
send_to_slave :hgetall
|
91
|
+
send_to_slave :hkeys
|
92
|
+
send_to_slave :hlen
|
93
|
+
send_to_slave :hmget
|
94
|
+
send_to_slave :hvals
|
95
|
+
send_to_slave :keys
|
96
|
+
send_to_slave :lindex
|
97
|
+
send_to_slave :llen
|
98
|
+
send_to_slave :lrange
|
99
|
+
send_to_slave :mget
|
100
|
+
send_to_slave :randomkey
|
101
|
+
send_to_slave :scard
|
102
|
+
send_to_slave :sdiff
|
103
|
+
send_to_slave :sinter
|
104
|
+
send_to_slave :sismember
|
105
|
+
send_to_slave :smembers
|
106
|
+
send_to_slave :sort
|
107
|
+
send_to_slave :srandmember
|
108
|
+
send_to_slave :strlen
|
109
|
+
send_to_slave :sunion
|
110
|
+
send_to_slave :ttl
|
111
|
+
send_to_slave :type
|
112
|
+
send_to_slave :zcard
|
113
|
+
send_to_slave :zcount
|
114
|
+
send_to_slave :zrange
|
115
|
+
send_to_slave :zrangebyscore
|
116
|
+
send_to_slave :zrank
|
117
|
+
send_to_slave :zrevrange
|
118
|
+
send_to_slave :zscore
|
119
|
+
|
120
|
+
# Send everything else to master.
|
121
|
+
def method_missing(name, *args, &block) # :nodoc:
|
122
|
+
if writable_master.respond_to?(name)
|
123
|
+
Client.send(:send_to_master, name)
|
124
|
+
send(name, *args, &block)
|
125
|
+
else
|
126
|
+
super
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
|
132
|
+
def make_client(config)
|
133
|
+
case config
|
134
|
+
when String
|
135
|
+
# URL like redis://localhost:6379.
|
136
|
+
uri = URI.parse(config)
|
137
|
+
Redis.new(:host => uri.host, :port => uri.port)
|
138
|
+
when Hash
|
139
|
+
# Hash of Redis client options (string keys ok).
|
140
|
+
redis_config = {}
|
141
|
+
config.each do |key, value|
|
142
|
+
redis_config[key.to_sym] = value
|
143
|
+
end
|
144
|
+
Redis.new(config)
|
145
|
+
else
|
146
|
+
# Hopefully a client object.
|
147
|
+
config
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
alias writable_master master
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module RedisMasterSlave
|
2
|
+
#
|
3
|
+
# Clients with no master defined are extended with this module.
|
4
|
+
#
|
5
|
+
# Attempts to access #writable_master will raise a ReadOnly::Error.
|
6
|
+
#
|
7
|
+
module ReadOnly
|
8
|
+
def writable_master
|
9
|
+
raise ReadOnlyError, "no master available"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
ReadOnlyError = Class.new(RuntimeError)
|
14
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'spec/spec_helper'
|
2
|
+
|
3
|
+
describe RedisMasterSlave do
|
4
|
+
describe "#initialize" do
|
5
|
+
it "should accept a single configuration hash" do
|
6
|
+
client = RedisMasterSlave::Client.new('redis://localhost:6479', ['redis://localhost:6480'])
|
7
|
+
mc, scs = client.master.client, client.slaves.map{|s| s.client}
|
8
|
+
[mc.host, mc.port].should == ['localhost', 6479]
|
9
|
+
scs.map{|sc| [sc.host, sc.port]}.should == [['localhost', 6480]]
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should accept URI strings" do
|
13
|
+
client = RedisMasterSlave::Client.new('redis://localhost:6479', ['redis://localhost:6480'])
|
14
|
+
mc, scs = client.master.client, client.slaves.map{|s| s.client}
|
15
|
+
[mc.host, mc.port].should == ['localhost', 6479]
|
16
|
+
scs.map{|sc| [sc.host, sc.port]}.should == [['localhost', 6480]]
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should accept Redis configuration hashes" do
|
20
|
+
client = RedisMasterSlave::Client.new({:host => 'localhost', :port => 6479},
|
21
|
+
[{:host => 'localhost', :port => 6480}])
|
22
|
+
mc, scs = client.master.client, client.slaves.map{|s| s.client}
|
23
|
+
[mc.host, mc.port].should == ['localhost', 6479]
|
24
|
+
scs.map{|sc| [sc.host, sc.port]}.should == [['localhost', 6480]]
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should accept Redis client objects" do
|
28
|
+
master = Redis.new(:host => 'localhost', :port => 6479)
|
29
|
+
slave = Redis.new(:host => 'localhost', :port => 6480)
|
30
|
+
client = RedisMasterSlave::Client.new(master, [slave])
|
31
|
+
client.master.should equal(master)
|
32
|
+
client.slaves.size.should == 1
|
33
|
+
client.slaves.first.should equal(slave)
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should accept multiple slaves" do
|
37
|
+
master = Redis.new(:host => 'localhost', :port => 6479)
|
38
|
+
slave0 = Redis.new(:host => 'localhost', :port => 6480)
|
39
|
+
slave1 = Redis.new(:host => 'localhost', :port => 6481)
|
40
|
+
client = RedisMasterSlave::Client.new(master, [slave0, slave1])
|
41
|
+
client.master.should equal(master)
|
42
|
+
client.slaves.size.should == 2
|
43
|
+
client.slaves[0].should equal(slave0)
|
44
|
+
client.slaves[1].should equal(slave1)
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should set index to 0" do
|
48
|
+
client = RedisMasterSlave::Client.new('redis://localhost:6479', ['redis://localhost:6480'])
|
49
|
+
client.index.should == 0
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should not have a master connection if no master configuration is given" do
|
53
|
+
client = RedisMasterSlave::Client.new(nil, ['redis://localhost:6480'])
|
54
|
+
client.master.should be_nil
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe "read operations" do
|
59
|
+
it "should go to each slave, round-robin" do
|
60
|
+
client = RedisMasterSlave::Client.new(master, [slave0, slave1])
|
61
|
+
master.set 'a', 'am'
|
62
|
+
slave0.set 'a', 'a0'
|
63
|
+
slave1.set 'a', 'a1'
|
64
|
+
client.get('a').should == 'a0'
|
65
|
+
client.get('a').should == 'a1'
|
66
|
+
client.get('a').should == 'a0'
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should work for read-only clients" do
|
70
|
+
client = RedisMasterSlave::Client.new(nil, [slave0])
|
71
|
+
slave0.set 'a', '1'
|
72
|
+
client.get('a').should == '1'
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe "other operations" do
|
77
|
+
it "should hit the master" do
|
78
|
+
client = RedisMasterSlave::Client.new(master, [slave0, slave1])
|
79
|
+
client.set 'a', 'z'
|
80
|
+
client.master.get('a').should == 'z'
|
81
|
+
client.slaves.map{|s| s.get('a')}.should == [nil, nil]
|
82
|
+
end
|
83
|
+
|
84
|
+
it "should raise a ReadOnlyError for read-only clients" do
|
85
|
+
client = RedisMasterSlave::Client.new(nil, [slave0])
|
86
|
+
lambda do
|
87
|
+
client.set 'a', '1'
|
88
|
+
end.should raise_error(RedisMasterSlave::ReadOnlyError)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'erb'
|
2
|
+
require 'tempfile'
|
3
|
+
|
4
|
+
module Support
|
5
|
+
module Redis
|
6
|
+
def self.included(base)
|
7
|
+
base.before { connect_to_redises }
|
8
|
+
base.before { quit_from_redises }
|
9
|
+
end
|
10
|
+
|
11
|
+
def connect_to_redises
|
12
|
+
master_port = ENV['REDIS_MASTER_SLAVE_MASTER_PORT'] || 6479
|
13
|
+
slave0_port = ENV['REDIS_MASTER_SLAVE_SLAVE0_PORT'] || 6480
|
14
|
+
slave1_port = ENV['REDIS_MASTER_SLAVE_SLAVE1_PORT'] || 6481
|
15
|
+
@master = ::Redis.new(:host => 'localhost', :port => master_port)
|
16
|
+
@slave0 = ::Redis.new(:host => 'localhost', :port => slave0_port)
|
17
|
+
@slave1 = ::Redis.new(:host => 'localhost', :port => slave1_port)
|
18
|
+
@master.flushdb
|
19
|
+
@slave0.flushdb
|
20
|
+
@slave1.flushdb
|
21
|
+
end
|
22
|
+
|
23
|
+
def quit_from_redises
|
24
|
+
@master.quit
|
25
|
+
@slave0.quit
|
26
|
+
@slave1.quit
|
27
|
+
end
|
28
|
+
|
29
|
+
attr_reader :master, :slave0, :slave1
|
30
|
+
end
|
31
|
+
end
|
metadata
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: redis_master_slave
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- George Ogata
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-03-10 00:00:00 -05:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: redis
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 3
|
30
|
+
segments:
|
31
|
+
- 0
|
32
|
+
version: "0"
|
33
|
+
type: :runtime
|
34
|
+
version_requirements: *id001
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: ritual
|
37
|
+
prerelease: false
|
38
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - "="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
hash: 23
|
44
|
+
segments:
|
45
|
+
- 0
|
46
|
+
- 2
|
47
|
+
- 0
|
48
|
+
version: 0.2.0
|
49
|
+
type: :development
|
50
|
+
version_requirements: *id002
|
51
|
+
description:
|
52
|
+
email:
|
53
|
+
- george.ogata@gmail.com
|
54
|
+
executables: []
|
55
|
+
|
56
|
+
extensions: []
|
57
|
+
|
58
|
+
extra_rdoc_files:
|
59
|
+
- LICENSE
|
60
|
+
- README.markdown
|
61
|
+
files:
|
62
|
+
- lib/redis_master_slave/client.rb
|
63
|
+
- lib/redis_master_slave/read_only.rb
|
64
|
+
- lib/redis_master_slave/version.rb
|
65
|
+
- lib/redis_master_slave.rb
|
66
|
+
- LICENSE
|
67
|
+
- README.markdown
|
68
|
+
- Rakefile
|
69
|
+
- CHANGELOG
|
70
|
+
- spec/redis_master_slave/client_spec.rb
|
71
|
+
- spec/spec_helper.rb
|
72
|
+
- spec/support/redis.rb
|
73
|
+
has_rdoc: true
|
74
|
+
homepage: http://github.com/oggy/redis_master_slave
|
75
|
+
licenses: []
|
76
|
+
|
77
|
+
post_install_message:
|
78
|
+
rdoc_options: []
|
79
|
+
|
80
|
+
require_paths:
|
81
|
+
- lib
|
82
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ">="
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
hash: 3
|
88
|
+
segments:
|
89
|
+
- 0
|
90
|
+
version: "0"
|
91
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
92
|
+
none: false
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
hash: 23
|
97
|
+
segments:
|
98
|
+
- 1
|
99
|
+
- 3
|
100
|
+
- 6
|
101
|
+
version: 1.3.6
|
102
|
+
requirements: []
|
103
|
+
|
104
|
+
rubyforge_project:
|
105
|
+
rubygems_version: 1.6.2
|
106
|
+
signing_key:
|
107
|
+
specification_version: 3
|
108
|
+
summary: Redis master-slave client for Ruby.
|
109
|
+
test_files:
|
110
|
+
- spec/redis_master_slave/client_spec.rb
|
111
|
+
- spec/spec_helper.rb
|
112
|
+
- spec/support/redis.rb
|