redis_master_slave 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3 @@
1
+ == 0.0.1 2011-03-10
2
+
3
+ * Hi.
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.
@@ -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.
@@ -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,11 @@
1
+ module RedisMasterSlave
2
+ VERSION = [0, 0, 1]
3
+
4
+ class << VERSION
5
+ include Comparable
6
+
7
+ def to_s
8
+ join('.')
9
+ end
10
+ end
11
+ 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
@@ -0,0 +1,11 @@
1
+ ROOT = File.expand_path('..', File.dirname(__FILE__))
2
+
3
+ $:.unshift "#{ROOT}/lib"
4
+ require 'redis_master_slave'
5
+ require 'rspec'
6
+
7
+ require 'spec/support/redis'
8
+
9
+ RSpec.configure do |c|
10
+ c.include Support::Redis
11
+ end
@@ -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