redis_ring 0.0.2 → 0.1.0

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,42 @@
1
+ module RedisRing
2
+ class SlaveRPC
3
+
4
+ attr_reader :http_client
5
+
6
+ def initialize(http_client)
7
+ @http_client = http_client
8
+ end
9
+
10
+ def connection(host, port)
11
+ Connection.new(http_client, host, port)
12
+ end
13
+
14
+ class Connection
15
+
16
+ attr_reader :http_client, :host, :port
17
+
18
+ def initialize(http_client, host, port)
19
+ @http_client = http_client
20
+ @host = host
21
+ @port = port
22
+ end
23
+
24
+ def join
25
+ http_client.post(host, port, "/slave/join")
26
+ end
27
+
28
+ def status
29
+ JSON.parse(http_client.get(host, port, "/slave/status"))
30
+ end
31
+
32
+ def start_shard(shard_no)
33
+ http_client.post(host, port, "/slave/start_shard/#{shard_no}")
34
+ end
35
+
36
+ def stop_shard(shard_no)
37
+ http_client.post(host, port, "/slave/stop_shard/#{shard_no}")
38
+ end
39
+
40
+ end
41
+ end
42
+ end
@@ -1,3 +1,3 @@
1
1
  module RedisRing
2
- VERSION = "0.0.2"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -1,14 +1,75 @@
1
1
  module RedisRing
2
2
 
3
+ class WebInterfaceRunner
4
+
5
+ include RedisRing::BackgroundThread
6
+
7
+ attr_reader :master, :slave
8
+
9
+ def initialize(port, master, slave)
10
+ @port = port
11
+ @master = master
12
+ @slave = slave
13
+ end
14
+
15
+ def do_work
16
+ handler = Rack::Handler.get("webrick")
17
+ handler.run(WebInterface, :Port => @port, :master => @master, :slave => @slave) do |server|
18
+ @server = server
19
+ WebInterface.set :master, master
20
+ WebInterface.set :slave, slave
21
+ WebInterface.set :running, true
22
+ end
23
+ end
24
+
25
+ def halt
26
+ super
27
+ @server.stop if @server
28
+ end
29
+
30
+ end
31
+
3
32
  class WebInterface < Sinatra::Base
4
33
 
34
+ def master
35
+ self.class.master
36
+ end
37
+
38
+ def slave
39
+ self.class.slave
40
+ end
41
+
5
42
  get "/" do
6
43
  "RedisRing is running"
7
44
  end
8
45
 
9
- get "/shards" do
46
+ post "/master/node_joined/:node_id" do
47
+ master.node_joined(params[:node_id])
48
+ "OK"
49
+ end
50
+
51
+ get "/slave/status" do
10
52
  content_type :json
11
- Application.instance.shards_hash.to_json
53
+ slave.status.to_json
54
+ end
55
+
56
+ post "/slave/join" do
57
+ slave.join
58
+ "OK"
59
+ end
60
+
61
+ post "/slave/start_shard/:shard_no" do
62
+ slave.start_shard(params[:shard_no].to_i)
63
+ "OK"
64
+ end
65
+
66
+ post "/slave/stop_shard/:shard_no" do
67
+ slave.stop_shard(params[:shard_no].to_i)
68
+ "OK"
69
+ end
70
+
71
+ class << self
72
+ attr_accessor :master, :slave
12
73
  end
13
74
 
14
75
  end
@@ -0,0 +1,73 @@
1
+ module RedisRing
2
+
3
+ class ZookeeperConnection
4
+
5
+ attr_reader :current_node
6
+
7
+ def initialize(host_name, base_port, zookeeper_address)
8
+ @host_name = host_name
9
+ @base_port = base_port
10
+ @zookeeper_address = zookeeper_address
11
+ @connected = false
12
+ @base_path = "/nodes"
13
+ @mutex = Mutex.new
14
+ end
15
+
16
+ def nodes_changed?
17
+ return true unless nodes_watcher
18
+ return nodes_watcher.completed?
19
+ end
20
+
21
+ def nodes
22
+ @nodes_watcher = Zookeeper::WatcherCallback.new
23
+ resp = zookeeper.get_children(:path => base_path, :watcher => nodes_watcher, :watcher_context => base_path)
24
+ return resp[:children].sort
25
+ end
26
+
27
+ def node_data(node)
28
+ resp = zookeeper.get(:path => "#{base_path}/#{node}")
29
+ data = resp[:data]
30
+ return data ? JSON.parse(data) : nil
31
+ end
32
+
33
+ def update_status(status)
34
+ zookeeper.set(:path => "#{base_path}/status", :data => status.to_json)
35
+ end
36
+
37
+ def connected?
38
+ @connected
39
+ end
40
+
41
+ def connect
42
+ @mutex.synchronize do
43
+ break if connected?
44
+
45
+ @zookeeper = Zookeeper.new(zookeeper_address)
46
+
47
+ if @zookeeper.state != Zookeeper::ZOO_CONNECTED_STATE
48
+ raise "Zookeeper not connected!"
49
+ end
50
+
51
+ resp = @zookeeper.create(:path => base_path)
52
+ #raise "Could not create base path" unless resp[:rc] == Zookeeper::ZOK
53
+
54
+ resp = @zookeeper.create(:path => "#{base_path}/node-", :ephemeral => true, :sequence => true, :data => current_node_data.to_json)
55
+ #raise "Could not create node" unless resp[:rc] == Zookeeper::ZOK
56
+
57
+ @current_node = resp[:path].gsub("#{base_path}/", '')
58
+
59
+ @connected = true
60
+ end
61
+ end
62
+
63
+ protected
64
+
65
+ attr_reader :zookeeper, :base_path, :nodes_watcher, :zookeeper_address, :host_name, :base_port
66
+
67
+ def current_node_data
68
+ {:host => host_name, :port => base_port}
69
+ end
70
+
71
+ end
72
+
73
+ end
@@ -0,0 +1,47 @@
1
+ module RedisRing
2
+
3
+ class ZookeeperObserver
4
+
5
+ include RedisRing::BackgroundThread
6
+
7
+ attr_reader :master, :slave, :zookeeper_connection
8
+
9
+ def initialize(zookeeper_connection, master, slave)
10
+ @zookeeper_connection = zookeeper_connection
11
+ @master = master
12
+ @slave = slave
13
+ @current_master = nil
14
+ end
15
+
16
+ def do_work
17
+ on_node_list_changed(zookeeper_connection.nodes) if zookeeper_connection.nodes_changed?
18
+ sleep(0.1)
19
+ end
20
+
21
+ protected
22
+
23
+ def on_node_list_changed(new_nodes)
24
+ current_master = new_nodes.first
25
+
26
+ unless @current_master == current_master
27
+ @current_master = current_master
28
+
29
+ if current_master == zookeeper_connection.current_node
30
+ master.became_master
31
+ else
32
+ master.no_longer_is_master
33
+ end
34
+
35
+ current_master_data = zookeeper_connection.node_data(current_master)
36
+ slave.current_master_host = current_master_data["host"]
37
+ slave.current_master_port = current_master_data["port"]
38
+
39
+ puts "NEW MASTER IS: #{slave.current_master_host}:#{slave.current_master_port}"
40
+ end
41
+
42
+ master.nodes_changed(new_nodes)
43
+ end
44
+
45
+ end
46
+
47
+ end
data/lib/redis_ring.rb CHANGED
@@ -2,17 +2,28 @@ require 'socket'
2
2
  require 'yaml'
3
3
  require 'erb'
4
4
  require 'fileutils'
5
+ require 'net/http'
5
6
 
6
7
  require 'sinatra'
7
8
  require 'json'
8
9
  require 'daemons'
10
+ require 'zookeeper'
9
11
 
10
12
  require 'monkey_patches'
11
13
 
14
+ require 'redis_ring/background_thread'
12
15
  require 'redis_ring/configuration'
13
16
  require 'redis_ring/shard_config'
14
17
  require 'redis_ring/shard'
18
+ require 'redis_ring/http_client'
19
+ require 'redis_ring/node'
15
20
  require 'redis_ring/application'
16
21
  require 'redis_ring/web_interface'
17
22
  require 'redis_ring/process_manager'
23
+ require 'redis_ring/master'
24
+ require 'redis_ring/master_rpc'
25
+ require 'redis_ring/slave'
26
+ require 'redis_ring/slave_rpc'
27
+ require 'redis_ring/zookeeper_observer'
28
+ require 'redis_ring/zookeeper_connection'
18
29
  require 'redis_ring/cli'
data/redis_ring.gemspec CHANGED
@@ -17,9 +17,11 @@ Gem::Specification.new do |s|
17
17
  s.add_dependency 'sinatra'
18
18
  s.add_dependency 'json'
19
19
  s.add_dependency 'daemons-mikehale'
20
+ s.add_dependency 'zookeeper'
20
21
 
21
22
  s.add_development_dependency 'rspec'
22
23
  s.add_development_dependency 'mocha'
24
+ s.add_development_dependency 'simplecov'
23
25
 
24
26
  s.files = `git ls-files`.split("\n")
25
27
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
@@ -0,0 +1,224 @@
1
+ class ClusterBuilder
2
+
3
+ def initialize
4
+ yield self
5
+ end
6
+
7
+ def nodes
8
+ @nodes ||= {}
9
+ end
10
+
11
+ def node(node_id)
12
+ nodes[node_id] ||= NodeBuilder.new(node_id, self)
13
+ end
14
+
15
+ def node_ids
16
+ return nodes.keys.sort
17
+ end
18
+
19
+ def start_shard(node_id, shard_number)
20
+ start_shard_callbacks.each do |block|
21
+ block.call(node_id, shard_number)
22
+ end
23
+ end
24
+
25
+ def stop_shard(node_id, shard_number)
26
+ stop_shard_callbacks.each do |block|
27
+ block.call(node_id, shard_number)
28
+ end
29
+ end
30
+
31
+ def on_start_shard(&block)
32
+ start_shard_callbacks << block
33
+ end
34
+
35
+ def on_stop_shard(&block)
36
+ stop_shard_callbacks << block
37
+ end
38
+
39
+ def start_shard_callbacks
40
+ @start_shard_callbacks ||= []
41
+ end
42
+
43
+ def stop_shard_callbacks
44
+ @stop_shard_callbacks ||= []
45
+ end
46
+
47
+ def fake_provider
48
+ @fake_provider ||= FakeNodeProvider.new(self)
49
+ end
50
+
51
+ def fake_connection
52
+ @fake_connection ||= FakeConnection.new(self)
53
+ end
54
+
55
+ class FakeNodeProvider
56
+ def initialize(cluster_builder)
57
+ @cluster_builder = cluster_builder
58
+ @count = 0
59
+ end
60
+
61
+ def new(host, port)
62
+ result = @cluster_builder.nodes.detect{|_, node| node.get.host == host && node.get.port == port }
63
+ return result[1].get if result
64
+ return @cluster_builder.node("unknown-node-#{@count += 1}").host(host).port(port).reachable(false).get
65
+ end
66
+ end
67
+
68
+ class FakeConnection
69
+ def initialize(cluster_builder)
70
+ @cluster_builder = cluster_builder
71
+ end
72
+
73
+ def node_data(node_id)
74
+ return nil unless @cluster_builder.nodes.key?(node_id)
75
+ node = @cluster_builder.node(node_id).get
76
+ return {"host" => node.host, "port" => node.port}
77
+ end
78
+
79
+ def update_status(status)
80
+ @status = status
81
+ end
82
+ end
83
+
84
+ class NodeBuilder
85
+ def initialize(node_id, cluster_builder)
86
+ @node_id = node_id
87
+ @joined = true
88
+ @running_shards = []
89
+ @available_shards = {}
90
+ @host = "localhost"
91
+ @port = 6400
92
+ @reachable = true
93
+ @cluster_builder = cluster_builder
94
+ end
95
+
96
+ def host(str)
97
+ @host = str
98
+ self
99
+ end
100
+
101
+ def port(int)
102
+ @port = int
103
+ self
104
+ end
105
+
106
+ def reachable(bool)
107
+ @reachable = bool
108
+ self
109
+ end
110
+
111
+ def joined(bool)
112
+ @joined = bool
113
+ self
114
+ end
115
+
116
+ def running_shards(arr)
117
+ @running_shards = arr
118
+ self
119
+ end
120
+
121
+ def running_shard(shard)
122
+ @running_shards << shard unless @running_shards.include?(shard)
123
+ self
124
+ end
125
+
126
+ def not_running_shard(shard)
127
+ @running_shards.delete(shard)
128
+ self
129
+ end
130
+
131
+ def available_shards(hash)
132
+ @available_shards = hash
133
+ self
134
+ end
135
+
136
+ def available_shard(shard, timestamp)
137
+ @available_shards[shard] = timestamp
138
+ self
139
+ end
140
+
141
+ def not_available_shard(shard)
142
+ @available_shards.delete(shard)
143
+ self
144
+ end
145
+
146
+ def load_properties(node)
147
+ node.load(props)
148
+ end
149
+
150
+ def props
151
+ {
152
+ :host => @host,
153
+ :port => @port,
154
+ :node_id => @node_id,
155
+ :running_shards => @running_shards,
156
+ :available_shards => @available_shards,
157
+ :joined => @joined,
158
+ :reachable => @reachable,
159
+ :cluster_builder => @cluster_builder
160
+ }
161
+ end
162
+
163
+ def get
164
+ @node ||= FakeNode.new(self)
165
+ end
166
+ end
167
+
168
+ class FakeNode
169
+
170
+ attr_reader :host, :port, :node_id
171
+
172
+ def initialize(builder)
173
+ @builder = builder
174
+ @builder.load_properties(self)
175
+ end
176
+
177
+ def load(opts)
178
+ @joined = opts[:joined]
179
+ @running_shards = opts[:running_shards]
180
+ @available_shards = opts[:available_shards]
181
+ @reachable = opts[:reachable]
182
+ @host = opts[:host]
183
+ @port = opts[:port]
184
+ @node_id = opts[:node_id]
185
+ @cluster_builder = opts[:cluster_builder]
186
+ end
187
+
188
+ def joined?
189
+ @joined
190
+ end
191
+
192
+ def available_shards
193
+ @available_shards
194
+ end
195
+
196
+ def running_shards
197
+ @running_shards
198
+ end
199
+
200
+ def start_shard(shard_number)
201
+ ensure_reachable
202
+ @running_shards << shard_number unless @running_shards.include?(shard_number)
203
+ @cluster_builder.start_shard(@node_id, shard_number)
204
+ end
205
+
206
+ def stop_shard(shard_number)
207
+ ensure_reachable
208
+ @running_shards.delete(shard_number)
209
+ @cluster_builder.stop_shard(@node_id, shard_number)
210
+ end
211
+
212
+ def update_status!
213
+ @builder.load_properties(self)
214
+ ensure_reachable
215
+ end
216
+
217
+ protected
218
+
219
+ def ensure_reachable
220
+ raise "Node #{@node_id} #{@host}:#{@port} is not reachable!" unless @reachable
221
+ end
222
+ end
223
+
224
+ end
@@ -0,0 +1,39 @@
1
+ class FakeHttpClient < RedisRing::HttpClient
2
+
3
+ def posts
4
+ @posts ||= []
5
+ end
6
+
7
+ def gets
8
+ @gets ||= []
9
+ end
10
+
11
+ def responses
12
+ @responses ||= Hash.new("OK")
13
+ end
14
+
15
+ def post(host, port, path, params = {})
16
+ url = uri(host, port, path, params).to_s
17
+ posts << url
18
+ return responses[url]
19
+ end
20
+
21
+ def get(host, port, path, params = {})
22
+ url = uri(host, port, path, params).to_s
23
+ gets << url
24
+ return responses[url]
25
+ end
26
+
27
+ def set_response(url, text)
28
+ responses[url] = text
29
+ end
30
+
31
+ def sent_post?(url)
32
+ posts.include?(url)
33
+ end
34
+
35
+ def sent_get?(url)
36
+ gets.include?(url)
37
+ end
38
+
39
+ end
@@ -0,0 +1,20 @@
1
+ class FakeMasterRPC
2
+
3
+ def connection(host, port)
4
+ @connections ||= {}
5
+ @connections["#{host}:#{port}"] ||= Connection.new
6
+ end
7
+
8
+ class Connection
9
+
10
+ def nodes_loaded
11
+ @node_loaded ||= []
12
+ end
13
+
14
+ def node_loaded(node_id)
15
+ nodes_loaded << node_id
16
+ end
17
+
18
+ end
19
+
20
+ end
@@ -0,0 +1,2 @@
1
+ class FakeNodeProvider
2
+ end
@@ -0,0 +1,19 @@
1
+ class FakeProcessManager
2
+
3
+ def started_shards
4
+ @started_shards ||= []
5
+ end
6
+
7
+ def start_shard(shard)
8
+ started_shards << shard
9
+ end
10
+
11
+ def stopped_shards
12
+ @stopped_shards ||= []
13
+ end
14
+
15
+ def stop_shard(shard)
16
+ stopped_shards << shard
17
+ end
18
+
19
+ end
@@ -0,0 +1,36 @@
1
+ class FakeSlaveRPC
2
+
3
+ def connection(host, port)
4
+ @connections ||= {}
5
+ @connections["#{host}:#{port}"] ||= Connection.new
6
+ end
7
+
8
+ class Connection
9
+
10
+ def status=(val)
11
+ @status = val
12
+ end
13
+
14
+ def status
15
+ @status ||= {"joined" => false, "running_shards" => [], "available_shards" => {}}
16
+ end
17
+
18
+ def started_shards
19
+ @started_shards ||= []
20
+ end
21
+
22
+ def start_shard(shard_number)
23
+ started_shards << shard_number
24
+ end
25
+
26
+ def stopped_shards
27
+ @stopped_shards ||= []
28
+ end
29
+
30
+ def stop_shard(shard_number)
31
+ stopped_shards << shard_number
32
+ end
33
+
34
+ end
35
+
36
+ end
@@ -0,0 +1,2 @@
1
+ class FakeZookeeperConnection
2
+ end
@@ -2,22 +2,24 @@ require File.expand_path("../../spec_helper", __FILE__)
2
2
 
3
3
  describe RedisRing::Application do
4
4
 
5
- describe "#shards_hash" do
6
- before(:each) do
7
- RedisRing::Shard.any_instance.stubs(:fork_redis_server => stub(:start => true, :stop => true, :running? => true))
8
- RedisRing::ShardConfig.any_instance.stubs(:save)
9
- RedisRing::ShardConfig.any_instance.stubs(:alive? => true)
5
+ before(:each) do
6
+ @conf = RedisRing::Configuration.from_yml_file(File.expand_path("../../test.conf", __FILE__))
7
+ @app = RedisRing::Application.new(@conf)
8
+ end
9
+
10
+ it "should run shards" do
11
+ @app.start
12
+ sleep(1)
10
13
 
11
- @application = RedisRing::Application.new(RedisRing::Configuration.new)
12
- @application.start
13
- end
14
+ @app.slave.join
15
+ sleep(3)
14
16
 
15
- it "should return all shards" do
16
- shard_hash = @application.shards_hash
17
+ @app.slave.running_shards.keys.sort.should == [0, 1, 2, 3]
18
+ end
17
19
 
18
- shard_hash[:count].should == @application.configuration.ring_size
19
- shard_hash[:shards].size.should == @application.configuration.ring_size
20
- end
20
+ after(:each) do
21
+ @app.stop
22
+ @app.wait
21
23
  end
22
24
 
23
25
  end
@@ -0,0 +1,20 @@
1
+ require File.expand_path("../../spec_helper", __FILE__)
2
+
3
+ describe RedisRing::MasterRPC do
4
+
5
+ before(:each) do
6
+ @http_client = FakeHttpClient.new
7
+ @host = "example.com"
8
+ @port = 666
9
+ @rpc = RedisRing::MasterRPC.new(@http_client).connection(@host, @port)
10
+ end
11
+
12
+ describe :node_joined do
13
+ it "should post to node joined url" do
14
+ @rpc.node_loaded("some_node_id")
15
+
16
+ @http_client.sent_post?("http://example.com:666/master/node_joined/some_node_id").should be_true
17
+ end
18
+ end
19
+
20
+ end