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.
@@ -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